100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Java Mastery
50 minintermediate

Core APIs Assessment and Practice

What You'll Build

In this exercise you will build a Cricket Statistics Toolkit, a console program that consolidates everything from Module 3: collections to store and organise player data, generics for type-safe containers, exception handling for robust input processing, and string and regex work for parsing and validating text. The toolkit reads player records from text input, validates them, stores them in appropriate collections, and answers queries like top scorers and team averages.

This pulls the module's separate topics into one coherent application where they must cooperate: a malformed input line must be caught and reported rather than crashing the program, records must be parsed with string operations and validated with regex, and the parsed data must be stored in the right generic collection for efficient lookup. By the end you will have a program that ingests messy real-world-style text and turns it into queryable, validated data, the essential shape of countless real applications.

Analogy🏏Cricket
🏏 Think of it like cricket: after drilling individual skills in the nets, batting, bowling, fielding, a player finally puts them together in a full practice match where the skills must work in combination under live conditions. Just as the practice match reveals whether the separately-drilled skills actually cohere into a performance, these two programs reveal whether your separately-learned constructs, types, operators, branches, loops, actually cohere into working software. Just as the match is where a player first feels how the pieces fit, the exercise is where you first feel how the language fits together. Just as a coach moves players from drills to matches once the basics are sound, the course moves you from concepts to construction once the fundamentals are in place. The insight is that fundamentals only become capability when you assemble them into something complete that runs.

Prerequisites

  • Completion of lessons 11 through 14: arrays and the Collections Framework, generics, exception handling, and strings and regex.
  • A JDK installed (17 or 21 recommended) with javac and java available; verify with java -version.
  • Comfort with List, Map, and Set and choosing among them, plus generic type parameters like List<Player> from lesson 11 and 12.
  • Understanding of try-catch and custom exceptions from lesson 13, and of StringBuilder, split, and regex patterns from lesson 14.
  • Familiarity with defining classes and methods from Module 2, since the toolkit models players as objects.

Setup & Project Structure

The toolkit is organised into a small set of classes: a Player record holding parsed data, a custom exception for invalid input, a parser that turns a text line into a Player, and a main class that drives ingestion and queries. Separating parsing, the data model, and the query logic keeps each concern testable and mirrors how real data pipelines are structured.

Analogy🏏Cricket
🏏 Think of it like cricket: setting up these two programs is like preparing two separate match fixtures, each with its own scorecard and playing eleven. Just as each fixture is self-contained — its own teams, its own result, no shared state that could tangle the outcomes — each program lives in its own clearly-named Java file with a single public class and a main method, the simplest structure for a console app. Just as the Laws require a clear team sheet before play begins so everyone knows who is on the field, the one-public-class-per-file rule keeps each program's entry point unambiguous. And just as scheduling two independent matches means a problem in one never delays the other, keeping the calculator and the number game in separate files keeps the exercises independent. The payoff: clean, isolated structure that lets you build, run, and reason about each program without the other getting in the way.

Lay out the files so the responsibilities are clear, then build incrementally: get the data model and parser working first, then ingestion with error handling, then the collection-backed queries. Confirm the project compiles at each stage so problems surface where they are introduced rather than accumulating.

bash
# Project layout for the Cricket Statistics Toolkit.
mkdir cricket-stats-toolkit && cd cricket-stats-toolkit

# Classes (each its own .java file with a matching public class):
#   Player.java                 -- the parsed data model (a record)
#   InvalidRecordException.java -- custom checked exception for bad input
#   RecordParser.java           -- text line -> Player, using regex + split
#   StatsToolkit.java           -- main: ingest, store in collections, answer queries

# Sample input lines the toolkit will ingest (name,team,runs,dismissals):
#   Virat Kohli,IND,12000,250
#   Rohit Sharma,IND,9500,210
#   BAD LINE WITHOUT COMMAS        <- must be rejected, not crash
#   Steve Smith,AUS,8500,abc       <- non-numeric runs/dismissals, must be rejected

# Compile everything together once the files exist:
#   javac *.java   then   java StatsToolkit

Step 1 — Foundation

Step 1 builds the data model and the custom exception. The Player is naturally immutable, parsed once and never changed, so a record (covered fully later, but used here as a concise immutable carrier) or a small final-field class fits well. The InvalidRecordException is a custom checked exception so that callers are forced to decide how to handle a bad line.

Establishing the data model and the failure type first means the rest of the program has a clear vocabulary: everything either produces a valid Player or raises an InvalidRecordException. This is the same discipline as defining your domain types and error conditions before writing the logic that uses them, so the logic reads in terms of meaningful concepts rather than raw strings and arrays.

Analogy🏏Cricket
🏏 Think of it like cricket: before drilling specific shots, a batter establishes their basic routine at the crease, take guard, face the ball, decide, repeat, the rhythm that frames every delivery. Just as that repeating routine frames each shot before the shot itself is chosen, the menu loop frames each operation before the operation is computed. Just as the batter always takes guard at least once before deciding whether to continue the innings, the do-while shows the menu at least once before checking whether to quit. Just as a sound routine lets the batter focus on each delivery cleanly, a sound interaction loop lets you focus on each operation cleanly. The insight is that establishing the repeating interaction structure first gives every later piece a clean frame to slot into.
java
// Player.java -- an immutable data model (record gives equals/hashCode/toString free).
public record Player(String name, String team, int runs, int dismissals) {
    // Batting average with a not-out guard (dismissals == 0).
    public double average() {
        return dismissals == 0 ? runs : (double) runs / dismissals;
    }
}

// InvalidRecordException.java -- custom CHECKED exception for malformed input.
public class InvalidRecordException extends Exception {
    public InvalidRecordException(String message) { super(message); }
}

Step 2 — Core Logic

Step 2 implements the parser, the heart of the toolkit, where strings, regex, and exceptions converge. The parser takes a raw text line, validates its overall shape with a regex, splits it into fields, converts the numeric fields with care, and either returns a Player or throws InvalidRecordException with a clear message explaining what was wrong.

This is the core because it is the boundary where untrusted text becomes trusted data, and getting it right means every downstream query operates on clean, validated records. The key discipline is converting the numeric fields inside a try and translating any NumberFormatException into your domain InvalidRecordException, so callers deal with one meaningful failure type rather than a mix of low-level exceptions, exactly the wrapping-with-cause pattern from lesson 13.

Analogy🏏Cricket
🏏 Think of it like cricket: the routine at the crease means nothing if the actual shots are mistimed, the core skill is making clean contact, and a batter who plays down the wrong line gets out no matter how good their routine. Just as clean contact is where runs are actually scored, correct arithmetic is where the calculator actually works. Just as misjudging the line of the ball, the equivalent of integer division truncating, produces a wrong result despite a good setup, forgetting to cast to double produces a wrong number despite a good menu. Just as a batter carefully watches for the ball that could dismiss them, you carefully guard against the zero denominator that would crash the calculation. The insight is that the core computation must be exactly right, because no amount of surrounding structure rescues a wrong result.
java
// RecordParser.java -- Step 2: validate with regex, split, convert, wrap failures.
import java.util.regex.Pattern;

public class RecordParser {
    // name (letters/spaces), team (3 uppercase), runs, dismissals -- compiled ONCE.
    private static final Pattern SHAPE =
            Pattern.compile("^[A-Za-z ]+,[A-Z]{3},\\d+,\\d+$");

    public static Player parse(String line) throws InvalidRecordException {
        if (line == null || !SHAPE.matcher(line.trim()).matches()) {
            throw new InvalidRecordException("Bad shape: '" + line + "'");
        }
        String[] f = line.trim().split(",");      // exactly 4 fields by the regex
        try {
            int runs = Integer.parseInt(f[2]);
            int dismissals = Integer.parseInt(f[3]);
            return new Player(f[0], f[1], runs, dismissals);
        } catch (NumberFormatException e) {
            // Wrap the low-level exception in our domain exception (cause preserved).
            throw new InvalidRecordException("Non-numeric field in: '" + line + "'");
        }
    }
}

Step 3 — Integration & Enhancement

Step 3 wires the parser into ingestion and builds the collection-backed queries. The main class reads each input line, calls the parser inside a try-catch so a bad line is reported and skipped rather than aborting the whole run, and stores each valid Player in collections chosen for the queries: a List for all players, and a Map from team to its players for grouped lookups.

This integrates the whole module: generics give type-safe collections (List<Player>, Map<String, List<Player>>), exception handling makes ingestion robust to bad data, and the queries use the collections' strengths, sorting the list for top scorers, grouping by the map for team averages. The enhancement is resilience: the program processes every good record and clearly reports every bad one, the hallmark of production data ingestion.

Analogy🏏Cricket
🏏 Think of it like cricket: a bowler searching for the right length to dismiss a batter does not know in advance how many balls it will take, they bowl, observe whether the ball was too short or too full, adjust, and repeat until they get it right. Just as the bowler loops an unknown number of times, adjusting from feedback each ball, the guessing game loops until correct, adjusting from higher/lower feedback each round. Just as 'too short, pitch it up' guides the bowler's next delivery, 'too low, guess higher' guides the player's next guess. Just as the bowler is guaranteed to eventually find the length, the game must guarantee the loop can end. The insight is that feedback-driven repetition of unknown length, the while loop's natural shape, models exactly this kind of iterative homing-in.
java
// StatsToolkit.java -- Step 3: resilient ingestion + collection-backed queries.
import java.util.*;

public class StatsToolkit {
    public static void main(String[] args) {
        String[] input = {
            "Virat Kohli,IND,12000,250", "Rohit Sharma,IND,9500,210",
            "BAD LINE", "Steve Smith,AUS,8500,abc", "Steve Smith,AUS,9000,180"
        };

        List<Player> all = new ArrayList<>();
        Map<String, List<Player>> byTeam = new HashMap<>();

        // Resilient ingestion: a bad line is reported and SKIPPED, not fatal.
        for (String line : input) {
            try {
                Player p = RecordParser.parse(line);
                all.add(p);
                byTeam.computeIfAbsent(p.team(), k -> new ArrayList<>()).add(p);
            } catch (InvalidRecordException e) {
                System.out.println("Skipped -> " + e.getMessage());
            }
        }

        // Query 1: top scorer (sort a copy by runs, descending).
        all.stream().max(Comparator.comparingInt(Player::runs))
           .ifPresent(p -> System.out.println("Top scorer: " + p.name()));

        // Query 2: average per team using the grouped Map.
        byTeam.forEach((team, players) -> {
            double avg = players.stream().mapToDouble(Player::average).average().orElse(0);
            System.out.printf("%s avg: %.1f%n", team, avg);
        });
    }
}

Step 4 — Testing & Verification

Verify the toolkit handles both clean and dirty input correctly. Confirm that valid lines become Player records, that the malformed line and the non-numeric line are each skipped with a clear message rather than crashing the program, and that the queries return correct results computed only from the valid records. Test the not-out edge case in the average, and confirm the team grouping places players under the right team. Compile all files together and run, checking the output against the expectations below.

Analogy🏏Cricket
🏏 Think of it like cricket: verifying your programs is like checking a scorecard against the video before it's official. Just as a scorer confirms a strike rate is a true decimal — 150.00, not a truncated 150 — you check each calculator operation returns a real decimal result, not an integer that silently drops the fraction. Just as the Laws guard against computing an economy rate for a bowler who hasn't bowled a single over, your zero-denominator guards prevent a divide-by-zero crash. Just as you'd test average, strike rate, and run rate with a known innings you can total by hand, you verify with values you can check yourself. And just as an umpire confirms a decision at the exact edge — level scores, an exact target — you test the higher/lower game's boundary where the guess equals the target. The payoff: normal and edge cases both proven correct, so the result stands up to scrutiny.
bash
# Compile all classes and run the toolkit.
javac *.java && java StatsToolkit

# Expected behaviour:
#   Skipped -> Bad shape: 'BAD LINE'                  (regex rejects it, no crash)
#   Skipped -> Non-numeric field in: 'Steve Smith,AUS,8500,abc'  (parse guard)
#   Top scorer: Virat Kohli                           (12000 is the max)
#   IND avg: ... AUS avg: ...                         (computed from VALID records only)
#
# Verification checklist:
#   [ ] The two bad lines are skipped with messages; the program still completes.
#   [ ] Valid records (including the second Steve Smith) are counted in queries.
#   [ ] Team averages use only successfully-parsed players.
#   [ ] A not-out player (dismissals == 0) does not cause a divide-by-zero.
#
# If a bad line ever crashes the run, the parse() call is not wrapped in try-catch.

Warning: A subtle bug in data ingestion is letting one bad record abort the entire batch. If the try-catch wraps the whole loop instead of the per-line parse call, the first malformed line stops all further processing, you lose every valid record after it. Always scope the try-catch to the smallest unit that can fail independently, here, a single line, so one bad record is isolated and the rest still process. Also ensure your regex anchors with ^ and $; without them, a partial match could let malformed lines slip through validation.

Extension Challenge: Add a query that returns the top N batters across all teams using a sorted collection, and a Set to detect and report duplicate player entries (same name and team appearing twice). Then make ingestion read from an actual file using try-with-resources from lesson 13, so the toolkit processes a real data file and closes it safely even if parsing throws. As a final touch, collect all rejected lines into a report rather than only printing them.

  • The toolkit combines all of Module 3: collections for storage, generics for type safety, exception handling for robust ingestion, and strings/regex for parsing and validation.
  • An immutable Player model and a custom checked InvalidRecordException give the program a clear vocabulary: every input yields a valid Player or a typed rejection.
  • The parser is the single validation checkpoint: it shape-checks with a compiled regex, splits fields, converts numbers, and wraps low-level failures in the domain exception.
  • Scoping the try-catch to a single line makes ingestion resilient, one bad record is reported and skipped while all valid records still process.
  • Queries use well-chosen generic collections: a List<Player> for overall ranking and a Map<String, List<Player>> for fast per-team grouping.
  • Verify both clean and dirty input: valid records become Players, malformed and non-numeric lines are skipped with clear messages, and queries compute only from valid data.
Lesson 15 of 35
0% complete