AP Computer Science A · Computer Science

AP Computer Science A

Java from your first compile to recursion, taught in the four units of the revised College Board framework. Every lesson shows real code, traces it line by line, and hands you a program to write; every unit ends with ten exam-style multiple-choice questions on a code stimulus; and the free-response section holds eight full FRQ prompts with the scoring notes a reader uses and a canonical nine-point solution to compare against.

3H EXAM 42 MCQ 4 FRQ 49 LESSONS 40 REVIEW QUESTIONS 8 FRQ PROMPTS PREREQ: ALGEBRA I

Course overview

What this course covers, and how the exam weights it.

AP Computer Science A follows the four units of the revised College Board framework. Unit 4 (data collections, 30–40%) and Unit 2 (selection and iteration, 25–35%) carry most of the exam's weight, and the four free-response questions come from them in a fixed order: methods and control structures, class design, data analysis with an ArrayList, and a 2D array. Section I is 42 multiple-choice questions in 90 minutes for 55% of the score; Section II is those four free-response questions in 90 minutes for the other 45%, each scored out of 9. Every unit below ends with a ten-question review in the exam's own multiple-choice style: a short program as the stimulus, then what is printed, how many times the loop runs, or which replacement makes the method work as intended. The FRQ section at the bottom of the outline holds eight prompts, two of each exam type, each with a coding space, the scoring notes, and the canonical solution explained point by point.

  • U1Using Objects and Methods15–25%
  • U2Selection and Iteration25–35%
  • U3Class Creation10–18%
  • U4Data Collections30–40%

All four units are open, 49 lessons in all. Every lesson pairs a short explanation with worked examples and a problem to try yourself, the same problem types that show up on the exam. Each unit closes with a short video walk-through and a ten-problem practice set with hidden answers.

Free preview: open any 5 lessons, or watch one unit video, without an account. The counter on the left keeps track.

Lesson 1.1 · Unit 1 · CED topic 1.1

Algorithms, programs, and how Java runs

An algorithm is a finite list of steps that solves a problem. Every algorithm you write this year is built from three kinds of step: sequence (do this, then that), selection (do this only if something is true), and iteration (do this again while something is true). That is the whole toolkit. A program is an algorithm written in a language a machine will accept.

Java takes a detour on its way to the machine. You write source code in a .java file; the compiler (javac) translates it into bytecode in a .class file; the Java Virtual Machine (java) executes that bytecode. That detour gives Java two very different kinds of failure. A compile-time error is caught by javac, so the program never runs at all; a run-time error gets past the compiler and blows up mid-execution, with output already on the screen.

Syntax

Every program the exam shows you sits inside this skeleton:

public class Main {
    public static void main(String[] args) {
        // statements run in order, top to bottom
    }
}

The file must be named after the class (Main.java here). Execution starts at the first statement of main and stops at its closing brace. Statements end in semicolons; blocks are bounded by braces.

Worked example · a program that runs

Convert 21.5 °C to Fahrenheit and print the result.

public class Main {
    public static void main(String[] args) {
        double celsius = 21.5;
        double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
        System.out.println(celsius + " C is " + fahrenheit + " F");
    }
}

Compiled with javac Main.java and run with java Main, it prints:

21.5 C is 70.7 F
Worked example · the same program, one semicolon short

Delete the semicolon after 21.5 and compile again. javac answers:

Main.java:3: error: ';' expected
        double celsius = 21.5
                             ^
1 error

No .class file is produced and nothing runs. Read the message as the compiler means it: line 3, the token it wanted, a caret under the exact column. The caret sits after the last good token, so the fault is at the end of the line named, not the start of the next one.

Worked example · classify four snippets

For each body of main, decide: compile-time error, run-time error, or runs fine?

snippetverdictwhat the tools actually say
int count = "seven";compile-timeerror: incompatible types: String cannot be converted to int
int t = 5; int g = 0; System.out.println(t / g);run-timeException in thread "main" java.lang.ArithmeticException: / by zero
System.out.println(3 + 4 * 2);runs fineprints 11
int price = 12; System.out.println("Total: " + totl);compile-timeerror: cannot find symbol; variable totl

Exam tip: the giveaway is who complains. A message opening with a file name and a line number came from the compiler. One opening with Exception in thread "main" came from the JVM: the code compiled, and the bug is in the logic.

Try it

This program has two separate defects. Name the first one the compiler reports, fix it, then say what happens when you run the fixed program.

public class Main {
    public static void main(String[] args) {
        int totalPages = 250;
        int pagesPerDay = 0
        System.out.println(totalPages / pagesPerDay);
    }
}
Show answer

The compiler never gets to the logic, so it reports only the missing semicolon:

Main.java:4: error: ';' expected
        int pagesPerDay = 0
                           ^
1 error

Add the semicolon and it compiles, but integer division by zero is a run-time failure:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Main.main(Main.java:5)

One fix, two lessons: a clean compile is not a correct program, and the compiler reports only what it can see from the grammar.

Lesson 1.2 · Unit 1 · CED topic 1.2

Variables and data types

A variable is a named box in memory, and in Java every box is stamped with a type before anything goes in it. That stamp is permanent: a box declared int holds whole numbers for the rest of its life. The compiler enforces the stamp, which is why so many Java bugs are caught before the program ever runs.

Four types carry almost all of Unit 1. Three are primitive (int for whole numbers, double for numbers with a decimal part, boolean for true/false), and they store the value itself. String is a reference type: the box holds a reference to a text object living elsewhere. So 7 and 7.0 are not the same value. They print differently, they divide differently, and they have different types.

Syntax

Declaration names a type; initialization puts the first value in the box:

int    quantity  = 3;          // whole number
double unitPrice = 12.50;      // number with a fractional part
boolean taxExempt = false;     // exactly true or false
String itemName  = "Lab notebook";   // text, in double quotes

Identifiers start with a letter and use camelCase: unitPrice, not UnitPrice or unit_price. Reserved words such as int, class, new, public and return cannot be used as names. Declaring the same name twice in the same block is a compile-time error.

Worked example · a receipt

Declare one variable of each type and print the receipt.

String itemName = "Lab notebook";
int quantity = 3;
double unitPrice = 12.50;
boolean taxExempt = false;

double subtotal = quantity * unitPrice;
System.out.println("Item:      " + itemName);
System.out.println("Quantity:  " + quantity);
System.out.println("Unit price " + unitPrice);
System.out.println("Subtotal:  " + subtotal);
System.out.println("Exempt:    " + taxExempt);

Run it:

Item:      Lab notebook
Quantity:  3
Unit price 12.5
Subtotal:  37.5
Exempt:    false

Note that 12.50 printed as 12.5. The literal's trailing zero is formatting, not part of the value: a double stores a number, never a number of decimal places.

Worked example · choosing the type
quantitytypedeclaration
students in a classintint classSize = 27;
body temperature in °Cdoubledouble bodyTempC = 37.4;
is the locker taken?booleanboolean lockerTaken = true;
a last nameStringString lastName = "Okafor";
a ZIP codeStringString zipCode = "02139";

The ZIP code is the interesting one. It is written with digits, but you never add two ZIP codes, and int would silently eat the leading zero. If arithmetic on it is meaningless, it is a String.

Worked example · trace the values

Starting from int points = 4;, double average = 2.5; and String label = "Quiz";, execute these five statements in order and record all three variables after each one. Output verified by running the program.

points = points + 3;
average = average * points;
label = label + points;
points = 10;
label = label + " " + average;
afterpointsaveragelabel
start42.5Quiz
stmt 172.5Quiz
stmt 2717.5Quiz
stmt 3717.5Quiz7
stmt 41017.5Quiz7
stmt 51017.5Quiz7 17.5

Statement 4 is the one students miss: changing points afterwards does nothing to average or label. An assignment copies the value at that instant; it does not create a permanent link.

Try it

Two of these four declarations will not compile. Which two, and what does the compiler say?

int seats = 4;
double price = 15.75;
boolean sold = "false";
String row = C;
Show answer

Lines 3 and 4. The real javac output:

Main.java:5: error: incompatible types: String cannot be converted to boolean
        boolean sold = "false";
                       ^
Main.java:6: error: cannot find symbol
        String row = C;
                     ^
  symbol:   variable C
  location: class Main
2 errors

"false" is text that looks like a boolean; the literal false has no quotes. And C without quotes is read as an identifier, so the compiler hunts for a variable named C. Write boolean sold = false; and String row = "C";.

Lesson 1.3 · Unit 1 · CED topic 1.3

Expressions and output

An expression is anything that evaluates to a single value. Java gives you five arithmetic operators (+, -, *, / and %), and the two that cause trouble are the last two. When both operands of / are int, Java does integer division: it throws the fractional part away rather than rounding, so 7 / 2 is 3 and 1 / 2 is 0. The modulus operator % hands you what was thrown away: the remainder.

That pair is not a nuisance; it is a tool. / and % together split any quantity into a whole part and a leftover, which is exactly what making change, splitting seconds into minutes, or peeling digits off a number requires.

Rule

Precedence runs *, /, % first (left to right), then + and -. Parentheses beat everything.

int whole     = total / size;    // int / int  -> int, truncated
int leftover  = total % size;    // the remainder
double exact  = total / 2.0;     // one double  -> the whole result is double

Mixing an int with a double promotes the result to double. System.out.println(x) prints x and moves to a new line; System.out.print(x) leaves the cursor where it is.

Worked example · making change

Turn 287 cents into coins using only / and %.

int cents = 287;

int dollars  = cents / 100;
int rest     = cents % 100;
int quarters = rest / 25;
rest         = rest % 25;
int dimes    = rest / 10;
rest         = rest % 10;
int nickels  = rest / 5;
int pennies  = rest % 5;

Printing each one gives:

2 dollars
3 quarters
1 dimes
0 nickels
2 pennies

The pattern is the whole point: divide to count the coins, mod to carry the leftover to the next line. Miss one % and every later count is wrong.

Worked example · expression, value, type

Every row below was printed by a real program, not predicted.

expressionprintstypewhy
7 / 23intboth operands int, so the .5 is truncated
7 % 21intthe remainder that division discarded
7.0 / 23.5doubleone double operand promotes the whole expression
1 + 2 + "x"3xStringleft to right: 1+2 is arithmetic, then 3 joins "x"
"x" + 1 + 2x12Stringleft to right: "x"+1 is already text, so 2 is appended
-7 / 2-3inttruncation is toward zero, not downward

Exam tip: the last two rows are a standing multiple-choice favorite. + means "add" only until one side is a String; from that point on, everything to the right is glued on as text. Parenthesize the arithmetic you want done first: "Total: " + (3 + 4) prints Total: 7, while "Total: " + 3 + 4 prints Total: 34.

Try it

A stopwatch reports int totalSeconds = 3725;. Write three expressions that compute hours, minutes and seconds, then state exactly what System.out.println(hours + ":" + minutes + ":" + seconds); prints.

Show answer

Divide down to each unit, and mod to strip the units already counted:

int hours   = totalSeconds / 3600;
int minutes = totalSeconds % 3600 / 60;
int seconds = totalSeconds % 60;

Running it prints:

1:2:5

3725 / 3600 is 1; 3725 % 3600 is 125, and 125 / 60 is 2; 3725 % 60 is 5. Nothing in that line is arithmetic once the first ":" appears, so the colons survive and no addition happens.

Lesson 1.4 · Unit 1 · CED topic 1.4

Assignment and keyboard input

In algebra, x = x + 3 is a false statement. In Java it is a perfectly ordinary instruction: evaluate the right side, then store the result in the box on the left. The = sign is not a claim about equality, it is a command, and it always runs right to left. That single rule explains most of what confuses people about reassignment.

It also explains why you cannot swap two variables by copying one into the other. The first assignment overwrites the value you still needed, and it is gone: there is no undo. You need a third box to hold it. Input works the same way: a Scanner pulls the next token the user typed and you assign it somewhere.

Syntax

Assignment, and the three-line swap that every exam expects you to know:

int temp = a;    // save what a holds
a = b;           // a now holds b's value
b = temp;        // b gets the saved value

import java.util.Scanner;               // at the top of the file
Scanner in = new Scanner(System.in);    // once, before any reading
int    n = in.nextInt();                // next whole number
double d = in.nextDouble();             // next decimal number
String s = in.nextLine();               // rest of the current line

nextInt and nextDouble stop at the end of the number and leave the newline sitting in the buffer. nextLine reads up to the next newline, which, right after a nextInt, is the leftover one.

Worked example · splitting a bill

Read a bill total and a party size, add an 18% tip, and print each share rounded to cents.

Scanner in = new Scanner(System.in);

System.out.println("Bill total?");
double bill = in.nextDouble();
System.out.println("Party size?");
int people = in.nextInt();

double withTip = bill * 1.18;
double share = withTip / people;
double rounded = (int) (share * 100 + 0.5) / 100.0;

System.out.println("With 18% tip: " + withTip);
System.out.println("Each person pays: " + rounded);

Typing 86.40 and then 4:

Bill total?
Party size?
With 18% tip: 101.952
Each person pays: 25.49

The raw share is 25.488, and (int)(25.488 * 100 + 0.5) / 100.0 is 2549 / 100.0, or 25.49. That idiom is the AP way to round to cents: Math.round is outside the subset.

Worked example · trace the swap

Start with a = 7 and b = 3. The table below is the real printed state after each statement, for the correct swap and for the naive two-statement version side by side.

statementabtempnaive xnaive y
start73: 73
1: temp = a; / x = y;73733
2: a = b; / y = x;33733
3: b = temp;37733

Look at the naive column at statement 1: the 7 is already destroyed. Statement 2 copies the 3 back, so both variables end at 3 and the 7 is unrecoverable. temp exists for exactly one reason, to survive that overwrite.

Try it

This program reads an age and then a full name. Typing 16 and then Maya Chen prints age=[16] name=[]. Explain the empty name and fix it with one added line.

Scanner in = new Scanner(System.in);
int age = in.nextInt();
String name = in.nextLine();
System.out.println("age=[" + age + "] name=[" + name + "]");
Show answer

nextInt consumed 16 but stopped before the newline that ended the line. nextLine then read from there to that newline and got nothing: the empty string. Consume the stranded newline first:

int age = in.nextInt();
in.nextLine();                  // throw away the rest of the number's line
String name = in.nextLine();

With the extra call, the same typed input prints:

age=[16] name=[Maya Chen]

Exam tip: whenever a question mixes nextInt or nextDouble with nextLine, check for the discarded line before you trust the output shown.

Lesson 1.5 · Unit 1 · CED topic 1.5

Casting and the range of a variable

Java will move a value into a roomier type on its own. Assign an int to a double and it widens quietly, because nothing can be lost. Going the other way loses information, so Java refuses unless you say, in writing, that you accept the loss. That written permission is a cast.

The two things students get wrong are what a cast does and where it goes. A cast to int truncates toward zero: it chops the fractional part off, it never rounds, and (int)(-7.9) is −7, not −8. And a cast binds tightly to the value on its immediate right, so putting it one step too late means the damage is already done.

Rule

Widening is automatic; narrowing needs a cast, and the cast applies before any surrounding operator.

double d = 5;            // widening: d is 5.0, no cast needed
int    n = (int) 5.9;    // narrowing: n is 5, the .9 is discarded

double avg = (double) sum / count;   // cast sum first, then divide  -> correct
double bad = (double) (sum / count); // divide as ints, then widen   -> too late

int rounded = (int) (x + 0.5);              // round half up (x >= 0)
double cents = (int) (x * 100 + 0.5) / 100.0;  // round to two decimals

An int holds roughly −2.1 billion to 2.1 billion (exactly −2147483648 through 2147483647). Pass the top and it wraps around to the bottom instead of raising an error.

Worked example · the average that refuses to be 82.6

Five scores (88, 79, 95, 71, 80) sum to 413. Averaging them looks trivial, and the first version is wrong.

int sum = 413;
int count = 5;
double average = sum / count;            // prints 82.0
double fixed1  = (double) sum / count;   // prints 82.6
double fixed2  = (double) (sum / count); // prints 82.0

The real output of all three:

no cast:     82.0
cast sum:    82.6
cast result: 82.0

In the first line both operands are int, so 413 / 5 is 82 and the double on the left only decorates it with a .0. The third line casts after the division, which cannot recover the 0.6 that is already gone. Only the middle line converts an operand before dividing.

Worked example · evaluate the casts
expressionprintsreason
(int) 7.97truncation, not rounding
(int) (-7.9)-7truncation is toward zero, so a negative rises
(double) 7 / 23.5cast binds to 7, so it is 7.0 / 2
7 / (double) 23.5one double operand is enough
(int) (9 / 2 + 0.5)49 / 2 is int 4 first, then 4.5 truncates to 4
(int) (9 / 2.0 + 0.5)54.5 + 0.5 is 5.0: the half-up idiom, done right
2147483647 + 1-2147483648overflow wraps silently; no exception

Exam tip: rows 5 and 6 are the classic distractor pair. Before you apply + 0.5, check that the division feeding it is not already integer division.

Try it

A quiz reports 17 correct out of 24. This prints 0.0. Say why, fix it with one cast, then round the result to two decimals.

int correct = 17;
int items = 24;
double pct = correct / items * 100;
Show answer

17 / 24 is integer division, so it is 0 before * 100 ever runs. Cast an operand of the division itself:

double pct = (double) correct / items * 100;
double toCents = (int) (pct * 100 + 0.5) / 100.0;

Running all three lines:

broken: 0.0
fixed:  70.83333333333334
to cents: 70.83

Casting the finished product, (double)(correct / items) * 100, still prints 0.0. The cast has to reach the division, not the answer.

Lesson 1.6 · Unit 1 · CED topic 1.6

Compound assignment and increment operators

Updating a variable from its own value is so common that Java gives it shorthand. price = price * 0.9; becomes price *= 0.9;, and count = count + 1; becomes count++;. The shorthand is not only shorter: it names the variable once, so you cannot accidentally update the wrong one.

There is a catch worth knowing before it bites you. A compound assignment carries a hidden cast back to the variable's own type. m = m + 2.7; on an int is a compile-time error, but m += 2.7; compiles happily and silently truncates. The shorthand is not just shorter; on an int it is quietly more permissive.

Syntax

Each operator combines an arithmetic step with an assignment back into the same variable:

x += 5;     // x = x + 5;
x -= 5;     // x = x - 5;
x *= 5;     // x = x * 5;
x /= 5;     // x = x / 5;   (int / int still truncates)
x %= 5;     // x = x % 5;

count++;    // count = count + 1;   on a line of its own
count--;    // count = count - 1;

The AP subset uses ++ and -- as complete statements only, never buried inside a larger expression like a[i++], because the value such an expression produces depends on which side the operator sits, and the exam will not test that.

Worked example · a price with a discount and a fee

A 24.00 item is marked down 10% and then charged a flat 1.50 handling fee.

double price = 24.00;
price *= 0.9;      // ten percent off
price += 1.50;     // flat fee

int stock = 12;
stock--;
stock++;
stock++;

Real output after each step:

list price   24.0
after 10% off 21.6
after fee     23.1
stock 11
stock 13

Order matters as much as the arithmetic. Swap the two lines and the fee gets discounted too, giving 22.95: a different, and probably illegal, receipt.

Worked example · int and double, side by side

The same five compound assignments are applied to int n = 7 and to double d = 7.0. Every row below is printed output.

statementn (int)d (double)
start77.0
*= 1.51010.5
+= 31313.5
/= 266.75
-= 155.75
%= 411.75

The first statement is where they part company: 7 × 1.5 is 10.5, and the hidden cast chops it to 10 in n. The second loss is at /= 2, where 13 / 2 is integer division. Two silent truncations turn 1.75 into 1, and neither one produced a warning.

Try it

Give the value of m after each statement, and say why the same program fails to compile if the second line is written the long way as m = m + 2.7;.

int m = 9;
m += 2.7;
m /= 4;
m *= 3;
Show answer

Printed after each statement:

start  m=9
+= 2.7 m=11
/= 4   m=2
*= 3   m=6

9 + 2.7 is 11.7, truncated to 11; 11 / 4 is 2 by integer division; 2 × 3 is 6. Written out in full, the compiler will not allow the same thing:

Main.java:4: error: incompatible types: possible lossy conversion from double to int
        m = m + 2.7;
              ^
1 error

Exam tip: a compound assignment onto an int is an implicit narrowing cast. If a question mixes += with a double literal, expect truncation on every line.

Lesson 1.7 · Unit 1 · CED topics 1.7–1.8

Using the Java API and documenting code

Almost nothing you need is written from scratch. A library is code somebody else wrote and tested; a package is a named folder of such classes; an import tells the compiler which package to look in. java.lang (home of Math, String and System) is imported for you. Scanner lives in java.util, so it needs one.

On the exam you will meet methods you have never seen, described by a short API entry, and you are expected to call them correctly anyway. An entry gives you four things: the header (return type, name, parameter types in order), what each parameter means, what the return value is, and a sentence of description. Everything you need to write the call is in the header.

Rule

Read the header left to right, then write the call that matches it, and document what you assume.

// API entry:  public static int max(int a, int b)
//   Returns the greater of a and b.
int best = Math.max(18, 23);          // best is 23

// API entry:  public String substring(int from, int to)
//   Returns the substring from index from, up to but NOT including to.
String part = "Programming".substring(0, 3);   // "Pro"

/**
 * Returns the larger of two quiz scores.
 * Precondition: both scores are at least 0.
 */
public static int betterScore(int a, int b) {
    return Math.max(a, b);
}

A precondition is what the caller must guarantee; a postcondition is what the method promises in return. Every exam question states its preconditions and you may assume them: checking a case the precondition rules out is wasted code.

Worked example · calling from the entry alone

Four entries, four calls, and the values they really produced.

API headercallvalue
static int max(int a, int b)Math.max(18, 23)23
static double max(double a, double b)Math.max(4.5, 4.25)4.5
String substring(int from)"Programming".substring(3)gramming
String substring(int from, int to)"Programming".substring(3, 7)gram

Notice the two max entries: same name, different parameter types. The compiler picks by what you pass, which is why the second call returns a double and the first an int.

Worked example · a method you have not met

The whole entry for compareTo: public int compareTo(String other): returns a negative integer if this string comes before other alphabetically, zero if they are equal, and a positive integer if it comes after. What do these print?

System.out.println("apple".compareTo("banana"));
System.out.println("banana".compareTo("apple"));
System.out.println("apple".compareTo("apple"));

Run it:

-1
1
0

The entry promised only a sign, so the safe reading is "negative, positive, zero", never "−1, 1, 0". The magnitude is undocumented; do not lean on it.

Try it

Another unfamiliar entry: public int indexOf(String str, int fromIndex): returns the index of the first occurrence of str at or after fromIndex, or −1 if there is none. For String word = "tomorrow"; write the single call that returns the index of the second letter o, and state one precondition the call relies on.

Show answer

The first o is at index 1, so start the search past it. Any fromIndex of 2 or 3 works:

int second = word.indexOf("o", 2);

Checked by running all three searches on the same word:

word.indexOf("o")      ->  1
word.indexOf("o", 2)   ->  3
word.indexOf("o", 4)   ->  6

Precondition: word is not null and fromIndex is a legal position in it. Postcondition: the result is a valid index at or after fromIndex, or −1, so handle the −1 rather than feed it into substring.

Lesson 1.8 · Unit 1 · CED topics 1.9–1.10

Method signatures and calling class methods

A method header is a contract written in five parts, and the compiler enforces every one of them. Read public static double milesToKm(double miles) from the left: public says anyone may call it, static says it belongs to the class rather than to an object, double is what it hands back, milesToKm is its name, and (double miles) is the one value it demands.

Arguments are matched to parameters by position and type, never by name. The variable you pass can be called anything; what matters is that the first argument fits the first parameter's type, the second fits the second, and there are exactly as many arguments as parameters. A void method returns nothing, so its call is a statement on its own line and cannot appear inside an expression.

Syntax

Write a static method in a class; call it through the class name.

public class Converter {
    public static double milesToKm(double miles) {
        return miles * 1.609344;
    }

    public static String formatLabel(String name, int count) {
        return name + " (" + count + ")";
    }
}

// from main, in another class:
double km = Converter.milesToKm(5.0);
System.out.println(Converter.formatLabel("Ridge Trail", 3));

Overloading means two methods in one class share a name but differ in the number or types of their parameters. The compiler chooses by looking at the arguments at the call site. Return type alone can never tell them apart.

Worked example · driving the Converter

Add roundTo(double value, int places), which uses the AP rounding idiom, and call everything from main.

public static double roundTo(double value, int places) {
    double factor = Math.pow(10, places);
    return (int) (value * factor + 0.5) / factor;
}

Printed output:

8.04672
8.05
Ridge Trail (3)
41.842944
41.8

The fourth line is Converter.milesToKm(26) with an int argument. That is legal: int widens to double on the way in, the same automatic conversion from Lesson 1.5. Narrowing never happens by itself.

Worked example · four calls, four different violations

Each of these breaks the contract in a different place. The real compiler output:

Main.java:3: error: incompatible types: String cannot be converted to double
        Converter.milesToKm("5");
Main.java:4: error: incompatible types: possible lossy conversion from double to int
        int k = Converter.milesToKm(5.0);
Main.java:5: error: incompatible types: int cannot be converted to String
        Converter.formatLabel(3, "Ridge Trail");
Main.java:6: error: cannot find symbol
        converter.milesToKm(5.0);
  symbol:   variable converter
callpart of the signature it violates
milesToKm("5")parameter type: a String is not a double, even one that looks numeric
int k = milesToKm(5.0)return type (the value is a double and will not narrow into an int
formatLabel(3, "Ridge Trail")argument order) position decides, so both arguments are wrong
converter.milesToKm(5.0)a static method is called on the class name, capital C, not on a variable
Try it

Overload formatLabel with a one-parameter version that returns the name in square brackets, then state what each of the three calls below prints.

System.out.println(Converter.formatLabel("Ridge Trail", 3));
System.out.println(Converter.formatLabel("Ridge Trail"));
System.out.println(Converter.formatLabel(Converter.formatLabel("Loop"), 2));
Show answer

The added header differs in parameter count, which is enough to overload:

public static String formatLabel(String name) {
    return "[" + name + "]";
}

Compiled and run, the three calls print:

Ridge Trail (3)
[Ridge Trail]
[Loop] (2)

Exam tip: the third call resolves inside out. The inner call takes one String, so the one-parameter version runs first and returns [Loop]; that String then becomes the first argument of the two-parameter version.

Lesson 1.9 · Unit 1 · CED topic 1.11

The Math class

Math is a class of static methods, so you never build a Math object: you call everything through the class name. The AP subset contains exactly four of its methods, and knowing that list is worth a question or two on the exam: Math.abs, Math.pow, Math.sqrt and Math.random. Anything else on a released question will be handed to you with its API entry.

Return types matter more here than anywhere else so far. Math.pow and Math.sqrt always return a double, even for a perfect square, so Math.sqrt(16) prints 4.0 and cannot be stored in an int without a cast. Math.abs is overloaded: pass it an int and you get an int back.

Rule

The four methods, and the one idiom the exam builds on Math.random:

Math.abs(x)        // absolute value; int in -> int out, double in -> double out
Math.pow(b, e)     // b raised to the power e; always a double
Math.sqrt(x)       // square root; always a double
Math.random()      // a double r with 0.0 <= r < 1.0  (1.0 is never produced)

// a random integer from min through max, both included:
int n = (int) (Math.random() * (max - min + 1)) + min;

Read the idiom in three steps: Math.random() fills the half-open interval from 0 up to but not including 1; multiplying stretches it to max - min + 1 slots; truncating collapses each slot to its whole number; adding min slides the block into place.

Worked example · distance and dice

The distance between (2, 3) and (10, 9) is built from Math.pow and Math.sqrt; a die is built from Math.random.

double dist = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));

int die1 = (int) (Math.random() * 6) + 1;
int die2 = (int) (Math.random() * 6) + 1;

Real output: the last line changes on every run, the others never do:

7
7.5
1024.0
4.0
1.4142135623730951
distance = 10.0
roll: 4 + 3 = 7

The first five lines are Math.abs(-7), Math.abs(-7.5), Math.pow(2, 10), Math.sqrt(16) and Math.sqrt(2). Only the first stays an int.

Worked example · a random integer from 5 through 20

There are 20 − 5 + 1 = 16 possible values, so the expression is (int) (Math.random() * 16) + 5. To prove both endpoints are reachable, substitute the extreme values Math.random() can return.

Math.random() returns× 16after (int)+ 5
0.0 (the smallest possible)0.005
0.06251.016
0.58.0813
0.99915.9841520
0.99999 (near the ceiling)15.999841520

Exam tip: the count max - min + 1 is where marks are lost. Writing * 15 here would silently make 20 impossible, and no error would ever appear: the bug only shows up as a value that never occurs.

Try it

Write one expression for a random even integer from 10 through 30 inclusive, and explain how you know 30 can occur.

Show answer

There are 11 even values (10, 12, … 30). Pick one of 11 slots, double it, and shift:

int n = (int) (Math.random() * 11) * 2 + 10;

The largest value Math.random() can approach is just under 1.0, so Math.random() * 11 stays just under 11.0 and truncates to at most 10. Then 10 × 2 + 10 = 30. The smallest case is 0 × 2 + 10 = 10. Every result is even because the doubling happens after the truncation: doubling first and truncating afterwards would let odd numbers through.

Lesson 1.10 · Unit 1 · CED topics 1.12–1.13

Objects, classes, and object creation

A class is a blueprint; an object is one thing built from it. The Rectangle class says every rectangle has a width and a height and can report its area. new Rectangle(3.0, 4.0) builds one actual rectangle in memory and runs the constructor, whose job is to give the new object its starting state.

Here is the part that trips people up. A variable of a class type does not contain the object. It contains a reference: the object's address. So b = a; copies an address, not a rectangle, and you end up with two names for one object. Change it through either name and both names see the change. That is aliasing, and the exam tests it constantly.

Syntax

Declare a reference, then point it at a new object:

Rectangle a = new Rectangle(3.0, 4.0);
//  type    name        constructor call with arguments

Rectangle b = a;     // b now refers to the SAME object as a
Rectangle c = null;  // c refers to no object at all

null is the reference that points nowhere. Calling a method on it throws a NullPointerException at run time: one of the few exceptions you are expected to recognize by name.

Worked example · two objects become one

Two rectangles are created, then one reference is assigned to the other and a mutator is called.

Rectangle a = new Rectangle(3.0, 4.0);
Rectangle b = new Rectangle(5.0, 2.0);
System.out.println("a area " + a.getArea() + ",  b area " + b.getArea());

b = a;
System.out.println("after b = a:  a area " + a.getArea() + ",  b area " + b.getArea());

b.setWidth(10.0);
System.out.println("after b.setWidth(10.0):  a area " + a.getArea() + ",  b area " + b.getArea());
System.out.println("a width " + a.getWidth() + ",  b width " + b.getWidth());

Real output:

a area 12.0,  b area 10.0
after b = a:  a area 12.0,  b area 12.0
after b.setWidth(10.0):  a area 40.0,  b area 40.0
a width 10.0,  b width 10.0

Nothing was ever done to a after line 1, yet its area changed. The 5-by-2 rectangle is still in memory but now unreachable: nothing refers to it.

Worked example · box-and-arrow trace

Track which name points at which object, and what each object holds.

statementa points atb points atobject 1 (w × h)object 2
a = new Rectangle(3.0, 4.0)object 1: 3.0 × 4.0:
b = new Rectangle(5.0, 2.0)object 1object 23.0 × 4.05.0 × 2.0
b = a;object 1object 13.0 × 4.0unreachable
b.setWidth(10.0);object 1object 110.0 × 4.0unreachable

Exam tip: when a question assigns one object variable to another, draw the two arrows before you answer anything. If both arrows land on the same box, every later mutator call affects both names.

Try it

Predict what this prints, including the exact run-time message, then say which line is the last one to produce output.

Rectangle c = null;
System.out.println("about to ask a null for its area");
System.out.println(c.getArea());
Show answer

Line 2 prints normally; line 3 fails because there is no object to run the method on.

about to ask a null for its area
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Rectangle.getArea()" because "c" is null
    at Main.main(Main.java:5)

The last line of real output is the message from line 2: a run-time error stops the program where it happens, and everything printed before it stays on screen. c was declared and initialized, so this is not a compile-time problem: null is a perfectly legal value for a reference.

Lesson 1.11 · Unit 1 · CED topic 1.14

Calling instance methods

An instance method belongs to an object, not to a class, so it needs an object to work on. You supply one with the dot operator: acct.deposit(75.50) means "run deposit on the object acct refers to". A static method like Math.sqrt needs no object, which is exactly why you write the class name in front of it instead.

The other thing to read off a header is whether the call produces a value. deposit is void: it changes the object and hands nothing back, so the call is a complete statement and can never appear inside a larger expression. getBalance returns a double, so its call is a value and can be printed, stored, or used in arithmetic.

Syntax

Given this class, every call goes through an object reference:

public class BankAccount {
    private double balance;
    public BankAccount(double startingBalance) { balance = startingBalance; }
    public void   deposit(double amount)  { balance = balance + amount; }
    public void   withdraw(double amount) { balance = balance - amount; }
    public double getBalance()            { return balance; }
}

BankAccount acct = new BankAccount(250.00);
acct.deposit(75.50);                      // void: a statement on its own
double twice = acct.getBalance() * 2;     // returns a value: usable in an expression

balance is private, so acct.balance is not available outside the class. The methods are the only way in: that is the whole point of the design.

Worked example · driving the account

Four calls on an account opened with 250.00, printing the balance after each.

BankAccount acct = new BankAccount(250.00);
acct.deposit(75.50);
acct.withdraw(40.00);
acct.deposit(14.50);
acct.withdraw(100.25);
double doubled = acct.getBalance() * 2;

Real output:

start          250.0
deposit 75.50  325.5
withdraw 40.00 285.5
deposit 14.50  300.0
withdraw 100.25 199.75
twice the balance is 399.5
Worked example · trace the balance

The same run as a table, which is the form the exam asks for.

callreturnsbalance after
new BankAccount(250.00)a reference250.0
acct.deposit(75.50)nothing (void)325.5
acct.withdraw(40.00)nothing (void)285.5
acct.deposit(14.50)nothing (void)300.0
acct.withdraw(100.25)nothing (void)199.75
acct.getBalance()199.75199.75 (unchanged)

Only the last row produces a value, and it is the only call that leaves the object exactly as it found it. An accessor reports; a mutator changes.

Try it

Starting from BankAccount acct = new BankAccount(80.00);, write four calls that leave the balance at exactly 152.75, using both deposit and withdraw at least once. Then say what the compiler does with System.out.println(acct.deposit(10.00));.

Show answer

One sequence that works: run and checked:

acct.deposit(100.00);
acct.withdraw(25.50);
acct.deposit(20.00);
acct.withdraw(21.75);

The balance printed after each call:

180.0
154.5
174.5
152.75

Printing a void call does not compile, and neither does calling an instance method on the class name:

Main.java:4: error: 'void' type not allowed here
        System.out.println(acct.deposit(10.00));
                                       ^
Main.java:5: error: non-static method getBalance() cannot be referenced from a static context
        BankAccount.getBalance();
2 errors

Exam tip: "which call causes a compile-time error" questions are usually one of these two: a void result used as a value, or an instance method called without an object.

Lesson 1.12 · Unit 1 · CED topic 1.15

String manipulation

A String is an object, and it is immutable: once built, its characters never change. Every String method therefore returns a new String and leaves the original alone. That single fact explains the most common beginner bug in this unit: calling a method and throwing the result away, then wondering why nothing happened.

Indexes run from 0 to length() - 1, and substring(from, to) includes from but stops before to. The AP subset has no charAt, so a single character is s.substring(i, i + 1): a one-character String, compared with equals, never with ==.

Rule

The whole String toolkit the exam uses:

s.length()              // number of characters
s.substring(from)       // from that index to the end
s.substring(from, to)   // from that index up to but NOT including to
s.indexOf(str)          // index of the first occurrence, or -1 if absent
s.equals(other)         // true when the characters match
s.compareTo(other)      // negative / zero / positive, alphabetical order
s + t                   // concatenation builds a brand-new String

== on two Strings compares references, not characters. It sometimes reports true by accident, which makes it the most dangerous kind of wrong. Use equals.

Worked example · initials from a full name

Find the space, take the letter before it and the letter after it.

String full = "Maya Chen";
int space = full.indexOf(" ");
String first = full.substring(0, 1);
String last  = full.substring(space + 1, space + 2);
System.out.println(first + "." + last + ".");

Real output:

space at index 4
M.C.

space + 1 is the first letter of the surname and space + 2 is one past it: that "one past" is what substring always wants as its second argument.

Worked example · the String table

Every value below was printed by a real run on String title = "Programming";.

callresultwhy
title.length()11count the characters; the last index is 10
title.substring(3)grammingindex 3 to the end
title.substring(0, 3)Proindexes 0, 1, 2: index 3 is excluded
title.indexOf("gram")3index where the match starts
title.indexOf("z")-1absent, and −1 is never a usable index
title.substring(3, 3)(empty)start at 3, stop before 3: zero characters
title.substring(3, 2)throwsStringIndexOutOfBoundsException: Range [3, 2) out of bounds for length 11

The last two rows are worth memorizing together: equal arguments are legal and give the empty String, but a second argument smaller than the first is a run-time crash.

Worked example · immutability and equality
String s = "cat";
s.substring(1);            // result discarded
System.out.println(s);     // cat

String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b);        // true
System.out.println(a == c);        // false
System.out.println(a.equals(c));   // true

Exam tip: a == b printed true only because Java reuses one shared object for identical literals. Change one to new String(...) and the same comparison flips to false while the characters are identical. Never compare Strings with ==.

Try it

For String email = "chen.maya@ascend.org"; write the statements that print the part before the @ and the part after it, without assuming where the @ is.

Show answer

Locate the separator once, then cut on both sides of it:

int at = email.indexOf("@");
String user = email.substring(0, at);
String host = email.substring(at + 1);

Real output:

at index 9
user: chen.maya
host: ascend.org

substring(0, at) stops before the @ and substring(at + 1) starts after it, so the separator itself appears in neither piece. If the address had no @, indexOf would return −1 and substring(0, -1) would throw, which is why a real program checks for the −1 first.

Unit 1 review · 10 multiple-choice

Unit 1 review: using objects and methods

Ten exam-style multiple-choice questions running in lesson order from how Java compiles and runs through expressions, casting, method signatures, the Math class, objects and Strings: every output below came from actually compiling and running the code, so click an option and read why it is right or wrong.

Multiple choice

  1. public class Main {
        public static void main(String[] args) {
            int count = 5;
            double rate = 0.5;
            int total = count * rate;
            System.out.println(total);
        }
    }

    Which of the following best describes the result of attempting to compile and run the program?

    Java truncates only when you ask it to with a cast. Nothing here says (int), so the compiler refuses the assignment rather than quietly throwing away the 0.5, and no line of the program ever runs.

    2.5 is the correct arithmetic value of count * rate, but the variable it is being stored in is declared int. A value's type and a variable's type must be compatible before the program can be built at all.

    Correct. One int times one double promotes to double, and a double will not fit in an int slot. javac reports it at line 5: incompatible types: possible lossy conversion from double to int. Writing int total = (int) (count * rate); compiles and prints 2.

    A run-time exception happens while the program is executing, which requires the program to have compiled first. This error is caught by the compiler, so there is no class file to run and no exception to throw.

  2. int cents = 287;
    System.out.println(cents / 100 + " dollars and " + cents % 100 + " cents");

    What is printed as a result of executing the code segment?

    Correct. Both operands of / are int, so 287 / 100 is 2 with the remainder discarded, and 287 % 100 is that discarded remainder, 87. The two operators are a matched pair: one gives the whole part, the other gives what is left over.

    This is what you would get if / behaved like a calculator. Integer division never produces a fractional part; to see 2.87 you would have to write cents / 100.0 so that one operand is a double.

    8 is the first digit of the remainder, not the remainder. % returns everything left after the last whole 100 is removed, 287 minus 200, which is 87, not 8.

    Neither operator leaves cents alone. This answer treats + as pure concatenation everywhere, but cents / 100 and cents % 100 are arithmetic expressions that are evaluated before anything is joined to a string.

  3. int x = 4;
    int y = 9;
    x = y;
    y = x;

    What are the values of x and y after the code segment executes?

    This is the swap the code was probably meant to perform, but it is not what happens. The decisive line is y = x;: by the time it runs, x already holds 9, so 9 is what gets copied back into y.

    Correct. Assignment copies a value at the moment it executes; it does not record a relationship. Line 3 overwrites the 4 in x, and that 4 is gone forever, so line 4 copies 9 into y. A real swap needs a third variable: int temp = x; x = y; y = temp;.

    These are the starting values, which would be right only if both assignments did nothing. Each = is a command to store, so both variables are written to.

    4 is the value x loses on line 3, and nothing ever puts it back. For y to end at 4, the original x would have to have been saved somewhere before line 3 overwrote it.

  4. double score = 82.6;
    int rounded = (int) (score + 0.5);
    int truncated = (int) score;
    System.out.println(rounded + " " + truncated);

    What is printed as a result of executing the code segment?

    Only the first cast rounds, and it rounds because of the + 0.5 written inside the parentheses, not because casting rounds. Line 3 has no + 0.5, so it cannot produce 83.

    This assumes both lines truncate identically, but the parentheses on line 2 change what gets truncated: 82.6 + 0.5 is 83.1, and the cast chops that to 83. Parentheses decide the order, so read them before you read the cast.

    Correct. Line 2 evaluates 83.1 first and then truncates it to 83; line 3 truncates 82.6 straight down to 82. This is the whole round-half-up idiom in two lines: (int) always drops the fraction, so you add 0.5 first when you want rounding.

    rounded is declared int, so it cannot hold 82.6, and the value stored in it is the result of the cast, which has no fractional part at all. The original score is never printed.

  5. int points = 7;
    points += 2.8;
    System.out.println(points);

    What is the value of points after the code segment executes?

    10 would require rounding 9.8 up, and nothing here rounds. The implicit cast that compound assignment performs behaves exactly like (int): it discards the fractional part instead of rounding it.

    Correct. points += 2.8; means points = (int) (points + 2.8);. The sum 9.8 is computed as a double and then narrowed, so the 0.8 is thrown away and 9 is stored: the silent truncation that makes compound assignment worth watching.

    points is declared int and an int variable can never hold 9.8. 9.8 is the intermediate value of the addition, not the value that survives the assignment.

    This is the difference between points += 2.8; and points = points + 2.8;. The second form really is a compile-time error; the compound form compiles because it carries a built-in narrowing cast.

  6. public class Converter {
        public static double milesToKm(double miles) {
            return miles * 1.609;
        }
    
        public static String formatLabel(String name, int count) {
            return name + ": " + count;
        }
    }

    Each of the following statements appears in the main method of another class. Which one causes a compile-time error?

    This compiles and returns 8.045. An int argument is widened to double automatically, because no information is lost going from 5 to 5.0: widening is the one conversion Java performs for you.

    This compiles and returns laps: 3. The first argument is a String and the second an int, matching the parameter list (String name, int count) position by position.

    Correct. Arguments are matched to parameters by position, never by name or by guesswork, so 3 lands on the String parameter. javac reports incompatible types: int cannot be converted to String. Swapping the two arguments fixes it.

    This compiles and returns 4.0225. A double literal passed to a double parameter needs no conversion at all, so it is the least problematic call of the four.

  7. // roll should end up holding a whole number
    // from 5 through 20, with every value possible
    int roll = /* missing code */;
    System.out.println(roll);

    Which replacement for /* missing code */ makes the segment work as intended for every possible value returned by Math.random?

    This comes from multiplying by the larger endpoint instead of the count of values. Running it 400,000 times produces values from 5 up to 24, so it overshoots the range by four.

    Correct. The range 5 through 20 contains 20 - 5 + 1, or 16, different values. Math.random() returns at least 0.0 and always less than 1.0, so Math.random() * 16 truncates to 0 through 15, and adding 5 shifts that to 5 through 20. Testing both ends confirms it: 0.0 gives 5, and 0.999999 gives 20.

    Using high - low instead of high - low + 1 loses one value. The truncated product runs 0 through 14, so the largest result is 19 and the value 20 can never occur.

    21 is the count of values from 0 through 20, but the range does not start at 0. Because 5 is added afterwards, this reaches 25, which is five past the intended top.

  8. public class Rectangle {
        private double width;
        private double height;
        public Rectangle(double w, double h) { width = w; height = h; }
        public void setWidth(double w) { width = w; }
        public double getWidth() { return width; }
        public double getArea() { return width * height; }
    }
    Rectangle a = new Rectangle(3.0, 4.0);
    Rectangle b = new Rectangle(5.0, 2.0);
    b = a;
    b.setWidth(10.0);
    System.out.println(a.getArea() + " " + b.getArea());

    What is printed as a result of executing the code segment?

    This treats b = a; as copying the rectangle rather than copying a reference. If there were still two objects, a would keep its area of 12.0, but after line 3 there is only one object with two names.

    Correct. Line 3 stores the address in a into b, so both variables now point at the 3.0-by-4.0 object and the 5.0-by-2.0 object becomes unreachable. Line 4 is the decisive step: setting the width through b changes the one shared object to 10.0 by 4.0, and both calls report 40.0.

    10.0 is the new width, not the area. getArea returns width * height, and the height of the surviving object is still 4.0.

    20.0 would be the area of the second rectangle after its width changed to 10.0, but that object was abandoned on line 3 and setWidth never touches it. Draw the two arrows before answering an aliasing question.

  9. public class BankAccount {
        private double balance;
        public BankAccount(double startingBalance) { balance = startingBalance; }
        public void deposit(double amount) { balance = balance + amount; }
        public void withdraw(double amount) { balance = balance - amount; }
        public double getBalance() { return balance; }
    }
    // in a different class, intended to return twice the account's balance
    public static double doubledBalance(BankAccount acct) {
        /* missing code */
    }

    Which replacement for /* missing code */ makes the method work as intended?

    balance is declared private, so it is visible only inside BankAccount. javac reports balance has private access in BankAccount; the accessor method is the only door into that field.

    getBalance is an instance method, so it needs an object, not a class name. The compiler says non-static method getBalance() cannot be referenced from a static context: a class name works only for static methods such as Math.sqrt.

    Correct. An instance method is called on a reference with the dot operator, and because getBalance returns a double, the call can sit inside a larger expression. Passing an account holding 325.50 returns 651.0.

    This writes the object as an argument instead of as the receiver. There is no method named getBalance in the class doing the calling, so the compiler reports cannot find symbol.

  10. String word = "Programming";
    System.out.println(word.substring(3, 7) + word.indexOf("gram"));

    What is printed as a result of executing the code segment?

    This starts counting at index 1 instead of 0. Index 0 is P, 1 is r, 2 is o and 3 is g, so substring(3, 7) begins at the g, not at the r after it.

    Five characters would come from substring(3, 8). The second argument of substring is the index where the piece stops, and that character is not included, so indexes 3, 4, 5 and 6 give exactly four characters.

    Correct. substring(3, 7) takes indexes 3 through 6 of Programming, which is gram, and indexOf("gram") returns the index where that match begins, which is 3. The + then joins a String to an int, so the int is converted to text and the line prints gram3 with no space.

    4 would be the position of gram if the first character were numbered 1. Java indexes Strings from 0, which is also why indexOf can use -1 as its "not found" signal.

Lesson 2.1 · Unit 2 · CED topic 2.1

Algorithms with selection and repetition

Unit 1 gave you sequence: statements that run once, top to bottom. That is enough to compute something, but not enough to decide anything or to keep working until a job is done. Unit 2 adds selection (run this only when a condition is true) and iteration (run this again while a condition is true). Those three shapes express every algorithm on this exam.

Write the algorithm in numbered English steps before you write any Java. English is quick to fix, and once the steps are right the translation is nearly mechanical: a step that says if becomes an if, a step that says until becomes a loop, everything else is a plain statement. Flowcharts say the same thing in pictures: a diamond is a decision, an arrow flowing backwards is a loop.

Rule

Every algorithm in this course is built from exactly three shapes:

total = total + price;        // sequence: runs once, in order

if (condition) {              // selection: runs 0 or 1 times
    // ...
}

while (condition) {           // iteration: runs 0, 1, or many times
    // ...
}

Choosing among them is one question: does this step happen always, sometimes, or repeatedly? Many problems need two of the three, and then the decision usually lives inside the loop.

Worked example · validating a PIN

Three tries, then lock the account. In English first:

  1. Set the attempt count to 0 and mark the account locked.
  2. While tries remain and it is still locked, do steps 3–5.
  3. Add one to the count and read the PIN typed.
  4. If it matches the stored PIN, mark the account unlocked.
  5. Otherwise report the rejected attempt.
  6. After the loop, report unlocked or locked out.

The same six steps in Java, with the control structures marked. The helper entry returns the PIN typed on that attempt:

int attempts = 0;                                   // step 1
boolean unlocked = false;
while (attempts < 3 && !unlocked) {                // step 2   ITERATION
    attempts++;                                     // step 3
    String typed = entry(attempts);
    if (typed.equals(correct)) {                    // step 4   SELECTION
        unlocked = true;
    } else {
        System.out.println("Attempt " + attempts + ": " + typed + " rejected");
    }
}
if (unlocked) {                                     // step 6
    System.out.println("Unlocked on attempt " + attempts);
} else {
    System.out.println("Locked out");
}

With the entries 4017, 4701, 4071 against a stored PIN of 4071, this prints:

Attempt 1: 4017 rejected
Attempt 2: 4701 rejected
Unlocked on attempt 3

attempts < 3 && !unlocked is a compound condition (Lesson 2.5); read it as "while tries remain and it is still locked." The selection sits inside the iteration, so the decision is made fresh on every pass.

Worked example · English steps to a Java skeleton

A warehouse charges 4.95 for handling plus 7.50 per box, where a box holds at most 20 kg, and adds 3.00 for zone 3. Five steps:

  1. Start the charge at 4.95 and the remaining weight at the order weight.
  2. While the remaining weight exceeds 20 kg, add 7.50 and subtract 20 kg.
  3. Add 7.50 for the last, partly filled box.
  4. If the zone is 3, add 3.00.
  5. Return the charge.

Step 1 names the parameters; steps 2 and 4 name the two blocks:

public static double shippingCharge(double weightKg, int zone) {
    double charge = 4.95;
    double remaining = weightKg;       // loop control variable: remaining
    while (remaining > 20.0) {
        charge = charge + 7.50;
        remaining = remaining - 20.0;  // the update that ends the loop
    }
    charge = charge + 7.50;
    if (zone == 3) {
        charge = charge + 3.00;
    }
    return charge;
}

Two calls, actually run:

53.0 kg, zone 3: 30.45
12.0 kg, zone 1: 12.45

Exam tip: whenever you sketch a loop, name its loop control variable and point at the line that changes it: here, remaining, changed by remaining = remaining - 20.0. A loop whose control variable never changes is the most common way to write a program that never stops.

Try it

A colony of 1200 bacteria is cut in half by each dose of a drug. Write the algorithm that counts how many doses are needed before the colony drops below 100, say which control structures it needs, and give the count.

Show answer

No decision at all: the only thing that varies is how many times the halving repeats, so this is pure iteration with a counter beside it.

public static int doses(int start, int floorValue) {
    int count = 0;
    int size = start;
    while (size >= floorValue) {
        size = size / 2;
        count++;
    }
    return count;
}

Printing size on each pass gives:

    after halving 1: 600
    after halving 2: 300
    after halving 3: 150
    after halving 4: 75
halvings = 4

Four doses. The test sits at the top of the loop, so the loop stops the moment size reaches 75: the fifth halving never happens, and count ends at 4, not 5.

Lesson 2.2 · Unit 2 · CED topic 2.2

Boolean expressions

A condition is not a special kind of grammar that only fits inside parentheses. It is an ordinary expression whose type happens to be boolean, and boolean has exactly two values: true and false. That means age >= 13 is a value in the same way that 3 + 4 is a value. You can print it, pass it to a method, return it, or park it in a variable and use it three lines later.

Doing exactly that is the mark of readable code. When a test is named: boolean oldEnough = age >= 13;: the reader learns what the test means, and you compute it once instead of retyping it in four places where three of them can drift out of date. Two comparisons need care: double values that came out of arithmetic almost never land on an exact target, and == on objects asks whether two references point at the same object, not whether they hold the same characters.

Syntax

Six relational operators, each producing a boolean you may store:

boolean isMember = true;            // a boolean holds exactly true or false

boolean equal     = a == b;         // equal to
boolean different = a != b;         // not equal to
boolean under     = a < b;          // less than
boolean atMost    = a <= b;         // less than or equal to
boolean over      = a > b;          // greater than
boolean atLeast   = a >= b;         // greater than or equal to

boolean sameName  = name.equals("Ana");   // objects: equals, never ==

Note == (a test) against = (an assignment). Writing if (x = 5) is a compile-time error in Java, which is one mercy the language does give you.

Worked example · an eligibility check

A club admits members aged 13 through 19. Name each piece of the rule instead of burying it in one line:

boolean isMember = true;
int age = 15;

boolean oldEnough = age >= 13;
boolean tooOld    = age > 19;

System.out.println("isMember:  " + isMember);
System.out.println("oldEnough: " + oldEnough);
System.out.println("tooOld:    " + tooOld);
System.out.println("age == 15: " + (age == 15));
System.out.println("age != 15: " + (age != 15));

Real output:

isMember:  true
oldEnough: true
tooOld:    false
age == 15: true
age != 15: false

The parentheses in the last two lines are required: + binds tighter than ==, so without them Java would try to compare a String with an int and refuse to compile. Joining the three named pieces into one rule is Lesson 2.5.

Worked example · five comparisons, three value sets

Every cell below was printed by a program, not predicted.

a, ba == ba != ba < ba <= ba > b
4, 7falsetruetruetruefalse
7, 7truefalsefalsetruefalse
9, 2falsetruefalsefalsetrue

The middle row is the one to memorize: at equality, < is false but <= is true. Nearly every off-by-one bug on this exam is a < that should have been <=, or the reverse.

Worked example · two comparisons that lie

Run these and read the output before you argue with it:

double x = 0.1 + 0.2;
System.out.println(x);
System.out.println(x == 0.3);
System.out.println(Math.abs(x - 0.3) < 0.000001);

String stored = "Ana";
String part = "An";
String typed = part + "a";
System.out.println(typed == stored);
System.out.println(typed.equals(stored));
0.30000000000000004
false
true
false
true

double arithmetic is done in binary, and one tenth has no exact binary form, so the sum lands a hair above 0.3. Never test two doubles with ==; test that the gap is small, as the third line does. And typed holds the characters Ana, yet typed == stored is false because the concatenation built a new String object at run time and == compares object addresses.

Try it

Predict all five values, then say which one depends on where the String came from.

int a = 5;
int b = 5;
double d = 0.1 * 3;
String s1 = "yes";
String s2 = "y" + "es";
String s3 = "y";
s3 = s3 + "es";

System.out.println(a == b);
System.out.println(d == 0.3);
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1.equals(s3));
Show answer

The real run:

true
false
true
false
true

a == b compares two int values, so it means what it says. d is 0.30000000000000004, so the second test fails. Lines 3 and 4 are the pair that matters: "y" + "es" is glued together by the compiler, which reuses the one shared "yes" object, so s1 == s2 is true, by accident. s3 is built at run time from a variable, so it is a different object and s1 == s3 is false even though both spell yes.

Exam tip: that is why == on Strings is worse than simply wrong: it is wrong intermittently. Only equals answers the question you actually meant.

Lesson 2.3 · Unit 2 · CED topic 2.3

if statements and control flow

An if statement guards a block: the block runs when the condition is true and is skipped entirely when it is false. Add else and you get a fork: exactly one of the two blocks runs, never both, never neither. Everything after the if statement resumes as normal, no matter which way the fork went.

Two pieces of punctuation cause most of the damage in this lesson. The braces are optional when the body is a single statement, which tempts people to leave them out and then add a second line later that quietly falls outside the guard. And a semicolon right after if (...) is legal Java: it makes the body an empty statement, so the block below it always runs. Neither mistake is a compile error, so the compiler will not save you. Write the braces every time.

Syntax

The guard, and the fork:

if (condition) {
    // runs only when condition is true
}

if (condition) {
    // exactly one of these two blocks runs
} else {
    // ...
}

There is no semicolon after the closing parenthesis and none after the closing brace. The condition must be a boolean; an int will not do, which is why if (count) does not compile in Java even though it does in some other languages.

Worked example · returning from inside an if

A grade band method, written as a run of separate if statements, each with a return inside it:

public static String letterGrade(int score) {
    System.out.println("checking " + score);
    if (score >= 90) {
        return "A";
    }
    if (score >= 80) {
        return "B";
    }
    if (score >= 70) {
        return "C";
    }
    System.out.println("no band matched");
    return "F";
}

Calling it with 91, 72 and 55 prints:

checking 91
returns A
checking 72
returns C
checking 55
no band matched
returns F
scorescore >= 90score >= 80score >= 70lines that ranreturns
91truenever testednever testedthe first if onlyA
72falsefalsetrueall three testsC
55falsefalsefalseall three, then the printlnF

return leaves the method on the spot. For 91 the two later conditions are never even evaluated, which is why the "no band matched" line printed for 55 and for nothing else.

Worked example · the stray semicolon

One character is different from the version this student meant to write:

public static String broken(int score) {
    String g = "F";
    if (score >= 90); {
        g = "A";
    }
    return g;
}

It compiles without a murmur, and then:

broken(91) returns A
broken(55) returns A

The semicolon is the body of the if: an empty statement that does nothing when the score is at least 90. The braces that follow are now just a plain block sitting in the method, so g = "A"; runs for every score. A method that returns the same answer for every input is the symptom; look for this semicolon first.

Worked example · the missing braces

Indentation is for humans; the compiler counts statements.

public static void noBraces(int score) {
    if (score >= 90)
        System.out.println("A band");
        System.out.println("certificate printed");
}

noBraces(55) prints:

certificate printed

Exam tip: a braceless if governs exactly one statement, the first one. The second println is outside the guard no matter how it is indented, so a failing score gets a certificate. On a free-response question, write the braces: graders read the code, not your intentions.

Try it

This method does not compile. What does javac say, why, and what is the fix?

public static int fee(int daysLate) {
    if (daysLate > 0)
        System.out.println("Late fee applies");
        return daysLate * 25;
    return 0;
}
Show answer

The real message:

L3b.java:6: error: unreachable statement
        return 0;
        ^
1 error

Only the println belongs to the if. The return daysLate * 25; therefore runs unconditionally, so nothing after it can ever execute, and the compiler refuses. Put both statements inside braces:

public static int fee(int daysLate) {
    if (daysLate > 0) {
        System.out.println("Late fee applies");
        return daysLate * 25;
    }
    return 0;
}

Now it compiles and behaves:

Late fee applies
fee(3) = 75
fee(0) = 0

This one is a lucky bug: the missing braces happened to produce unreachable code, so the compiler caught it. In the noBraces example above there was nothing unreachable, and the same mistake ran silently.

Lesson 2.4 · Unit 2 · CED topic 2.4

Nested conditionals and multi-way selection

Real rules rarely have two cases. When they have five, you have two ways to write them. An if / else if / else chain lays the cases side by side and guarantees that exactly one of them runs. Nesting puts one if inside another and is the right shape when the second question only makes sense after the first is answered: you ask the weight tier within a zone, because the tiers differ by zone.

A chain has a property that independent if statements do not: once a test passes, every later test is skipped. That is exactly what you want for mutually exclusive cases, and it makes the order of the tests part of the logic. Put the most specific test first. Write the same five cases as five separate if statements and each one is evaluated on its own, which is how one order ends up charged in four tiers at once.

Syntax

The chain, and the nest:

if (test1) {            // the chain: at most one block runs,
    // ...               // and the first true test wins
} else if (test2) {
    // ...
} else {
    // the catch-all; optional
}

if (outer) {            // the nest: the inner question is only
    if (inner) {        // asked when the outer answer was true
        // ...
    }
}

else if is not a keyword: it is an else whose single statement happens to be another if. That is why a chain and a set of nested else blocks are the same program written two ways.

Worked example · weight tiers inside zone tiers

Zone 1 is domestic, zone 2 is everything else, and each has three weight tiers:

public static double shippingCost(int zone, double kg) {
    double cost = 0.0;
    if (zone == 1) {
        if (kg <= 1.0) {
            cost = 4.50;
        } else if (kg <= 5.0) {
            cost = 7.25;
        } else {
            cost = 12.00;
        }
    } else {
        if (kg <= 1.0) {
            cost = 6.75;
        } else if (kg <= 5.0) {
            cost = 11.50;
        } else {
            cost = 18.90;
        }
    }
    return cost;
}

Six calls, actually run:

zone 1, 0.8 kg: 4.5
zone 1, 3.0 kg: 7.25
zone 1, 9.0 kg: 12.0
zone 2, 0.8 kg: 6.75
zone 2, 5.0 kg: 11.5
zone 2, 9.0 kg: 18.9

The 5.0 kg call lands on 11.50, not 18.90, because kg <= 5.0 includes the boundary. Every tiered problem has a boundary question like this one, and the exam loves testing the value that sits exactly on the line.

Worked example · five independent ifs charge four times

A rewards program gives one discount, decided by the order total. Written as five separate if statements that each add to the same accumulator:

public static double rewardBuggy(double total) {
    double off = 0.0;
    if (total >= 25.0)  { off = off + 2.00; }
    if (total >= 50.0)  { off = off + 5.00; }
    if (total >= 100.0) { off = off + 12.00; }
    if (total >= 200.0) { off = off + 25.00; }
    if (total >= 500.0) { off = off + 70.00; }
    return off;
}

Tracing a 250.00 order, with off printed after each test:

statementtestoff after
start, 0.0
if total >= 25.0true2.0
if total >= 50.0true7.0
if total >= 100.0true19.0
if total >= 200.0true44.0
if total >= 500.0false44.0

It returns 44.0 where 25.00 was intended: the order qualified for four tiers and was paid for all four. Turning the five statements into one chain, largest test first, stops after the first match and returns 25.0: verified by running both on the same input.

public static double reward(double total) {
    double off = 0.0;
    if (total >= 500.0) {
        off = 70.00;
    } else if (total >= 200.0) {
        off = 25.00;
    } else if (total >= 100.0) {
        off = 12.00;
    } else if (total >= 50.0) {
        off = 5.00;
    } else if (total >= 25.0) {
        off = 2.00;
    }
    return off;
}
Try it

This chain is a chain, and every test in it is correct. What does band(95) return, and why?

public static String band(int score) {
    if (score >= 70) {
        return "C";
    } else if (score >= 80) {
        return "B";
    } else if (score >= 90) {
        return "A";
    }
    return "F";
}
Show answer

The real run, for four scores:

band(95)  = C
band(85)  = C
band(75)  = C
band(55)  = F

A 95 satisfies score >= 70, and in a chain the first true test wins, so the method returns "C" and never looks at the other two. The later branches are unreachable in practice, no score can reach them, so "A" and "B" are never returned at all.

Reverse the order so the most demanding test is asked first:

if (score >= 90) {
    return "A";
} else if (score >= 80) {
    return "B";
} else if (score >= 70) {
    return "C";
}
return "F";

fixed(95) returns A and fixed(85) returns B. Exam tip: when a multiple-choice item shows a chain over numeric ranges, check the order of the tests before you check the comparisons: reordered tiers is the more common trap.

Lesson 2.5 · Unit 2 · CED topic 2.5

Compound boolean expressions

Three operators combine conditions. && ("and") is true only when both sides are true; || ("or") is true when at least one side is true; ! ("not") flips a single value. They let one condition say what four nested if statements would otherwise have to.

Java evaluates them left to right with short-circuiting: as soon as the answer is settled, the rest is never evaluated. A false left side of &&, or a true left side of ||, skips the right side entirely. That is not a detail you can ignore: it is the mechanism behind the guard idiom, where the left side checks that something is safe to touch and the right side touches it.

Rule

Three operators, and the precedence that decides how they group:

!p            // highest: applies to the one operand on its right
p && q        // then and
p || q        // then or, lowest of the three

// so   a || b && c   groups as   a || (b && c)

if (s != null && s.length() > 0) {    // the guard idiom: the left side
    // safe to use s here                 protects the right side
}

Relational operators bind tighter than all three, so age >= 16 && hasPermit needs no inner parentheses. Add parentheses anywhere the grouping is not obvious at a glance; they cost nothing and they are never wrong.

Worked example · leap years in one condition

A year is a leap year when it is divisible by 4, except that century years must also be divisible by 400. Three rules, one expression:

public static boolean isLeapYear(int year) {
    return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}

Four calls, actually run:

1900: false
2000: true
2023: false
2024: true

A subtlety worth checking rather than assuming: because && binds tighter, dropping the inner parentheses gives the same rule here, since any year divisible by 400 is divisible by 4. Running both over 1600–2400 confirms they never disagree. Keep the parentheses anyway: they say which rule you meant. The genuine bug is dropping the || year % 400 == 0 clause:

no-parens version agrees on 1600..2400: true
forgot-400 version agrees on 1600..2400: false
correct(2000)    = true
forgot400(2000)  = false
Worked example · a truth table for a real rule

A learner may drive when age >= 16 && (hasPermit || withAdult). Every value below came from running the method:

agehasPermitwithAdultage >= 16permit || adultwhole condition
17truefalsetruetruetrue
17falsefalsetruefalsefalse
15truetruefalsetruefalse
16falsetruetruetruetrue

Row 3 is the short-circuit case: age >= 16 is false, so Java never evaluates (hasPermit || withAdult) at all. The table shows what it would be; at run time it is simply not computed.

Worked example · why the order of the operands matters

The same two tests, written both ways, against a String that is null:

String note = null;
if (note != null && note.length() > 0) {
    System.out.println("has a note");
} else {
    System.out.println("no note printed - the guard worked");
}
System.out.println("now the operands the other way round:");
if (note.length() > 0 && note != null) {
    System.out.println("never reached");
}

The real run:

no note printed - the guard worked
now the operands the other way round:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
    at L5.main(L5.java:35)

Exam tip: when a question shows a null check next to a method call on the same reference, the null check must come first. Swapping the operands of && does not change the truth value, but it absolutely changes whether the program survives.

Try it

A library allows a checkout when the patron is a member, has fewer than 5 books out, and owes no fines. Write it as one compound condition. Then say whether a || b && c means (a || b) && c, and give values that prove it.

Show answer

All three tests joined with &&, the last one negated:

public static boolean canCheckOut(boolean isMember, int booksOut, boolean hasFines) {
    return isMember && booksOut < 5 && !hasFines;
}
member, 4 out, no fines: true
member, 5 out, no fines: false
member, 2 out, fines:    false
guest,  0 out, no fines: false

"Fewer than 5" is < 5, so 5 books already blocks the checkout. On precedence, && binds tighter than ||, so a || b && c is a || (b && c). With a = true, b = false, c = false:

a || b && c   = true
a || (b && c) = true
(a || b) && c = false

Same three values, two different answers. If you meant the third line, you have to type the parentheses.

Lesson 2.6 · Unit 2 · CED topic 2.6

Comparing and simplifying boolean expressions

Two conditions are equivalent when they produce the same value for every possible input, not for the inputs you happened to try. The exam tests this directly: it shows you a working condition and four proposed replacements and asks which ones keep the method working "for all values". The only way to answer with confidence is a truth table that includes the boundary cases.

Two simplifications are worth knowing by name. De Morgan's laws push a ! through a compound condition and flip the connector: the negation of "a and b" is "not a or not b", and the negation of "a or b" is "not a and not b". And any method shaped like if (test) return true; else return false; is just return test;: the condition is already a boolean, so wrapping it in an if to produce a boolean adds nothing.

Rule

De Morgan, and the redundant if:

!(p && q)   is equivalent to   !p || !q
!(p || q)   is equivalent to   !p && !q

// redundant
if (score >= 60) {
    return true;
} else {
    return false;
}

// the same method
return score >= 60;

Negating a relational operator flips it the same way: !(x >= lo) is x < lo. The trap is negating >= to <= instead of <, which is wrong only at the boundary, and the boundary is exactly where the exam looks.

Worked example · one method, three equivalent bodies

"Is x outside the range lo to hi, inclusive?"

public static boolean outOfRangeA(int x, int lo, int hi) {
    if (x < lo) {
        return true;
    } else if (x > hi) {
        return true;
    } else {
        return false;
    }
}

public static boolean outOfRangeB(int x, int lo, int hi) {
    return x < lo || x > hi;
}

public static boolean outOfRangeC(int x, int lo, int hi) {
    return !(x >= lo && x <= hi);
}

Version C is version B with De Morgan applied in reverse. Running all three on every x from −2 to 12 with lo = 1 and hi = 10:

all three versions agree for x = -2..12: true
outOfRangeB(0, 1, 10)  = true
outOfRangeB(1, 1, 10)  = false
outOfRangeB(10, 1, 10) = false
outOfRangeB(11, 1, 10) = true

The two middle calls are the ones that matter: an inclusive range means both endpoints are in, so 1 and 10 are not out of range. Version B is the one to write: shortest, and it never mentions true or false.

Worked example · which replacement works for all values?

A method flags a student as not passing with !(score >= 60 && attendance >= 0.8). Which of these four replacements behave identically?

  • I. score < 60 || attendance < 0.8
  • II. score < 60 && attendance < 0.8
  • III. !(score >= 60) || !(attendance >= 0.8)
  • IV. score <= 60 || attendance <= 0.8

Four rows, every value printed by a real run:

score, attendanceoriginalIIIIIIIV
75, 0.95falsefalsefalsefalsefalse
75, 0.60truetruefalsetruetrue
45, 0.95truetruefalsetruetrue
60, 0.80falsefalsefalsefalsetrue

I and III only. II fails because it kept && instead of flipping it to ||: it flags a student only when both requirements fail. IV agrees on the first three rows and breaks on the fourth, where the student is exactly at both cut-offs: <= should have been <.

Exam tip: a replacement that agrees on three ordinary rows and differs on the boundary is the standard distractor. Always include a row where each value sits exactly on the cut-off.

Try it

Rewrite this method with no !, no if, and no true or false literal in it.

public static boolean comfortable(int temp, int humid) {
    if (!(temp > 30 || humid > 70)) {
        return true;
    } else {
        return false;
    }
}
Show answer

De Morgan first: !(temp > 30 || humid > 70) becomes !(temp > 30) && !(humid > 70), and each negated relational flips to <=. Then drop the redundant if:

public static boolean comfortable(int temp, int humid) {
    return temp <= 30 && humid <= 70;
}

Running both versions on every temperature 20–40 and humidity 50–90:

agree for temp 20..40 and humidity 50..90: true
comfortableShort(28, 65) = true
comfortableShort(30, 70) = true
comfortableShort(31, 65) = false
comfortableShort(28, 71) = false

Watch the negation of >: it is <=, not <. At exactly 30 degrees and 70 percent the original returns true, and only <= reproduces that.

Lesson 2.7 · Unit 2 · CED topic 2.7

while loops

A while loop repeats a block for as long as a condition holds. The condition is tested before each pass, including the very first, so a loop whose condition starts out false runs zero times: that is a feature, not an edge case. Use while when you do not know in advance how many passes you need: keep dividing until the number is gone, keep reading until the sentinel arrives.

Every correct while loop does three jobs with its loop control variable, and they sit in three different places. Initialize it before the loop, test it in the header, and update it inside the body. Skip the initialization and the code will not compile; skip the update and the program runs forever. Almost every broken loop you will meet is missing one of the three.

Syntax

The header, and the three jobs around it:

int n = start;               // 1. initialize, before the loop
while (n > 0) {              // 2. test, at the top of every pass
    // do the work
    n = n / 10;              // 3. update, inside the body
}
// control arrives here the first time the test is false

The condition is checked once more after the last pass: that failing test is what ends the loop, so the loop control variable always leaves the loop holding a value that makes the condition false.

Worked example · counting the digits of a number

How many digits does a positive int have? Divide it by 10 until nothing is left, counting the divisions:

public int digitCount(int n) {
    int count = 0;
    while (n > 0) {
        n = n / 10;
        count++;
    }
    return count;
}

The trace for n = 4073, printed on every pass by a real run:

passncountn > 0
start40730true
14071true
2402true
343true
404false

digitCount(4073) returns 4. Integer division does the work: 4 / 10 is 0, not 0.4, which is exactly what makes the loop stop. digitCount(9) returns 1 and digitCount(1000000) returns 7.

Worked example · a sentinel-controlled loop

Read numbers until a special value, the sentinel, says the data is finished. The sentinel must be a value the real data can never take, here −1:

Scanner in = new Scanner(System.in);
int total = 0;
int readings = 0;
int value = in.nextInt();          // prime the loop: read one before testing
while (value != -1) {
    total = total + value;
    readings = readings + 1;
    value = in.nextInt();          // read the next one at the end of the body
}
System.out.println("readings: " + readings);
System.out.println("total: " + total);

Typing 12 8 5 -1 gives:

readings: 3
total: 25

The sentinel is counted by neither line: the loop tests it, finds it equal to −1, and stops before adding it. The first read outside the loop is called priming, without it, value has no value to test on the first pass and the code will not compile.

Worked example · delete the update, lose the program

The same digit counter with n = n / 10; removed:

int n = 4073;
int count = 0;
while (n > 0) {
    count++;
    System.out.println("pass " + count + ":   n = " + n + "   count = " + count);
}

The first six lines of a run that had to be killed from outside:

pass 1:   n = 4073   count = 1
pass 2:   n = 4073   count = 2
pass 3:   n = 4073   count = 3
pass 4:   n = 4073   count = 4
pass 5:   n = 4073   count = 5
pass 6:   n = 4073   count = 6

Exam tip: n never changes, so n > 0 is true forever. When a question asks "what is printed", check that the loop condition depends on something the body actually modifies: if it does not, the answer is "nothing, because the loop never terminates".

Try it

Write digitSum(int n), which returns the sum of the digits of a positive int, then trace it for 4073.

Show answer

n % 10 peels off the last digit and n / 10 removes it: the same loop as digitCount with one extra line:

public int digitSum(int n) {
    int sum = 0;
    while (n > 0) {
        sum = sum + n % 10;
        n = n / 10;
    }
    return sum;
}
passnsumn > 0
start40730true
14073true
24010true
3410true
4014false

digitSum(4073) returns 14. Pass 3 adds 0 and looks like it did nothing; it did exactly what it should. The order matters: take the remainder before you divide, or the digit is gone before you have added it.

Lesson 2.8 · Unit 2 · CED topic 2.8

for loops

A for loop is a while loop with the three jobs collected into one header, where you can see them all at once. Use it when the number of passes is known before the loop starts: count to n, walk the indexes of a String, step through a range. Use while when it is not.

The order in which the three parts run is worth saying out loud, because every "how many times" question depends on it: the initialization runs once; then the condition is tested; if it is true the body runs; then the update runs; then the condition is tested again. The update happens after the body, not before, and the final update is what makes the condition fail.

Syntax

The header and its equivalent while:

for (int i = 0; i < n; i++) {      // initialization; condition; update
    // body
}

int i = 0;                         // the same loop, spelled out
while (i < n) {
    // body
    i++;
}

A variable declared in the header exists only inside the loop. Declare it before the loop instead: int i; for (i = 0; ...), if you need to read its value afterwards.

Worked example · summing the multiples of 3

Add every multiple of 3 strictly below a limit. Step by 3 instead of testing every number:

public int sumMultiplesOfThree(int limit) {
    int sum = 0;
    for (int k = 3; k < limit; k = k + 3) {
        sum = sum + k;
    }
    return sum;
}

Real output, with the same method rewritten as a while loop for comparison:

sumMultiplesOfThree(20)  = 63
sumMultiplesOfThree(100) = 1683
sumMultiplesOfThree(3)   = 0
while version (20)       = 63
while version (100)      = 1683

For a limit of 20 the loop adds 3, 6, 9, 12, 15 and 18: 63. The third call is the instructive one: with limit = 3 the condition 3 < 3 is false on the very first test, the body never runs, and the method returns the sum it was initialized with.

Worked example · how many times, and what is left over

Three headers. For each, the iteration count, the loop variable on the last pass, and its value just after the loop: all read off a run that declared the variable outside the header so it could be printed afterwards.

headeriterationsvalue on last passvalue after the loop
i = 0; i < 5; i++545
j = 1; j <= 5; j++556
k = 10; k > 0; k = k - 341−2

Exam tip: the exam asks this directly, and the two right-hand columns are where people lose the point. The first two headers run the same number of times by different routes: 0 to n - 1 and 1 to n both give n passes. The countdown runs on 10, 7, 4 and 1, then updates to −2, which fails k > 0, so the value that ends the loop is not a value the body ever saw.

Try it

How many times does the body of this loop execute, what is i on the last pass, and what is i immediately after the loop ends?

for (i = 2; i <= 20; i = i + 4) {
    System.out.println("body runs with i = " + i);
}
Show answer

Five passes. The real output:

body runs with i = 2
body runs with i = 6
body runs with i = 10
body runs with i = 14
body runs with i = 18
iterations 5   last i 18   i after loop 22

i takes the values 2, 6, 10, 14, 18, and then the update makes it 22, which fails i <= 20 and ends the loop. Note that 20 satisfies the condition but is never reached, because the step of 4 walks straight past it.

Counting without running it: the values are 2 + 4t, and the largest one that is at most 20 is 18, at t = 4. Values t = 0 through t = 4 is five iterations.

Lesson 2.9 · Unit 2 · CED topic 2.9

Implementing selection and iteration algorithms

Almost every loop the exam asks you to write is one of four accumulator patterns: a running sum, a count of the items that match a condition, a running maximum or minimum, and an early return that stops as soon as an answer is known. Learn the four shapes and most free-response loops become a matter of filling in the condition.

Two decisions separate a working accumulator from a broken one. First, what the accumulator starts at: 0 for a sum, 0 for a count, and, this is the one people get wrong, the first value in the data for a maximum or a minimum. Second, where the return goes: after the loop when you must see everything, inside the loop when the first match settles the question.

Rule

The four patterns, with the initialization that makes each one correct:

int sum = 0;                     // running sum
int count = 0;                   // count matching
int max = value(lo);             // running maximum: the FIRST value, never 0
for (int k = lo + 1; k <= hi; k++) {
    if (value(k) > max) {
        max = value(k);
    }
}

for (int k = lo; k <= hi; k++) { // early return: stop at the first match
    if (matches(k)) {
        return k;
    }
}
return -1;                       // no match found

Before dividing a sum by a count, check that the count is not zero. An average over an empty set divides by zero, and Java handles that two different ways: int division throws ArithmeticException, while double division throws nothing and quietly produces NaN (0.0 over 0) or Infinity (anything else over 0): a nonsense answer that then spreads through the rest of the program.

Worked example · largest, count, and a guarded average

Let value(k) be 40 - (k - 7) * (k - 7). Over k = 1 to 12 it produces:

value(1..12): 4 15 24 31 36 39 40 39 36 31 24 15
public int largestValue(int lo, int hi) {
    int max = value(lo);
    for (int k = lo + 1; k <= hi; k++) {
        if (value(k) > max) {
            max = value(k);
        }
    }
    return max;
}

public double averageAbove(int lo, int hi, int threshold) {
    int sum = 0;
    int count = 0;
    for (int k = lo; k <= hi; k++) {
        if (value(k) > threshold) {
            sum = sum + value(k);
            count++;
        }
    }
    if (count == 0) {
        return -1.0;
    }
    return (double) sum / count;
}

Real output:

largestValue(1, 12) = 40
countAbove(1, 12, 30) = 7
averageAbove(1, 12, 30) = 36.0
averageAbove(1, 12, 100) = -1.0

Seven values exceed 30, and they average 36.0: note the cast: without (double), 252 / 7 would be int division. The last call finds nothing above 100, and the guard returns −1.0 instead of dividing by zero.

Worked example · why a maximum never starts at 0

Five winter readings, temp(day) = -3 * day - 1, every one of them negative:

temp(1..5): -4 -7 -10 -13 -16
public int maxTempWrong(int firstDay, int lastDay) {
    int max = 0;                                  // the bug
    for (int day = firstDay; day <= lastDay; day++) {
        if (temp(day) > max) {
            max = temp(day);
        }
    }
    return max;
}

Both versions, run on the same data:

maxTempWrong(1, 5) = 0
maxTempRight(1, 5) = -4

Zero is larger than every reading, so no reading ever replaces it and the method returns a temperature that was never recorded. Start the running maximum at the first element and loop from the second, then the answer is guaranteed to be a value that is actually in the data.

Exam tip: this is the single most-tested mistake in the accumulator family, and graders check it by feeding your method all-negative data. The mirror image bites a running minimum: starting a minimum at 0 with all-positive data returns 0. Running lowestWrong over the value data above returns 0, where the true minimum is 4.

Try it

Write firstDayBelow(int limit), which returns the first day from 1 to 5 whose temperature is below limit, or −1 if there is none. Say why the return belongs inside the loop, and give the results for limit = -9 and limit = -20.

Show answer

The question is settled by the first match, so returning immediately is both correct and faster, and it is the only way to report the first one rather than the last:

public int firstDayBelow(int limit) {
    for (int day = 1; day <= 5; day++) {
        if (temp(day) < limit) {
            return day;
        }
    }
    return -1;
}

Real output:

temp(1..5): -4 -7 -10 -13 -16
firstDayBelow(-9)  = 3
firstDayBelow(-20) = -1

Day 3 is the first reading below −9, and the loop stops there without looking at days 4 and 5. Nothing is below −20, so control reaches the return -1; after the loop. That final return is not optional, without it the method would not compile, because a path exists in which nothing is returned.

Lesson 2.10 · Unit 2 · CED topic 2.10

Implementing String algorithms

A String traversal is an ordinary for loop over indexes 0 to length() - 1. The AP subset has no charAt, so the character at index i is s.substring(i, i + 1): a one-character String, which means you compare it with equals, never with ==.

Because Strings are immutable, you never modify one in place. You build a new one, and which side of the + you put the new character on decides whether you get a copy or a reversal. That one-character choice is the whole difference between the two methods below.

Syntax

The traversal skeleton every String algorithm starts from:

for (int i = 0; i < s.length(); i++) {
    String ch = s.substring(i, i + 1);    // the character at index i
    if (ch.equals("a")) {
        // ...
    }
}

result = result + ch;   // appends: builds a copy
result = ch + result;   // prepends: builds the reversal

The bound is i < s.length(). Writing <= makes substring(i, i + 1) run one past the end and throws StringIndexOutOfBoundsException.

Worked example · countVowels and reverse

The same traversal, doing two different jobs:

public int countVowels(String s) {
    int count = 0;
    for (int i = 0; i < s.length(); i++) {
        String ch = s.substring(i, i + 1);
        if (ch.equals("a") || ch.equals("e") || ch.equals("i")
                || ch.equals("o") || ch.equals("u")) {
            count++;
        }
    }
    return count;
}

public String reverse(String s) {
    String result = "";
    for (int i = 0; i < s.length(); i++) {
        result = s.substring(i, i + 1) + result;
    }
    return result;
}

Real output:

countVowels("programming") = 3
countVowels("rhythm") = 0
reverse("class") = ssalc
reverse("") = []

reverse("") returns the empty String: length() is 0, the condition fails immediately, and the method returns the "" it started with. Accumulators that start at the identity value (0 for a sum, "" for a String) handle the empty case for free.

Worked example · tracing reverse on "class"

Every row printed by the loop as it ran:

is.substring(i, i + 1)result after the assignment
start: (empty)
0cc
1llc
2aalc
3ssalc
4sssalc

The loop walks forward, but each new character is glued to the front, so the string grows backwards. Swap the operands to result = result + ch; and the same loop returns class unchanged.

Worked example · a palindrome test that reuses reverse

Do not write a second traversal. A palindrome is a word equal to its own reversal:

public boolean isPalindrome(String s) {
    return s.equals(reverse(s));
}
isPalindrome("racecar") = true
isPalindrome("level") = true
isPalindrome("class") = false

Exam tip: s.equals(reverse(s)), never s == reverse(s). reverse builds a brand-new object every time, so the reference test would return false for "racecar": a wrong answer that looks like a working method until it is tested.

Try it

Write countOccurrences(String s, String target), which returns how many times target appears in s, using indexOf rather than a character-by-character traversal. Test it on "mississippi".

Show answer

Find a match, then cut the string just past where it started and search what is left. indexOf returns −1 when there is nothing more to find, which ends the loop:

public int countOccurrences(String s, String target) {
    int count = 0;
    String rest = s;
    int pos = rest.indexOf(target);
    while (pos >= 0) {
        count++;
        rest = rest.substring(pos + 1);
        pos = rest.indexOf(target);
    }
    return count;
}

Real output:

countOccurrences("mississippi", "ss") = 2
countOccurrences("mississippi", "i") = 4
countOccurrences("mississippi", "z") = 0

Cutting at pos + 1 rather than pos is what prevents an infinite loop: cut at pos and the same match is found again forever. The third call shows the empty answer: indexOf returns −1 on the first try, the body never runs, and count stays 0.

Lesson 2.11 · Unit 2 · CED topic 2.11

Nested iteration

A nested loop is a loop whose body contains another loop. The rule that explains all of their behavior is this: the inner loop runs to completion on every single pass of the outer loop. The inner loop control variable is re-initialized each time, counts all the way up, and finishes, before the outer variable advances by one.

When the inner bound does not mention the outer variable, the total number of inner passes is the product of the two counts. When it does, the total is a sum instead, and that is the case the exam asks about. Outer loop means rows, inner loop means the items within a row: the shape you will reuse for 2D arrays in Unit 4.

Syntax

The nest, and where the counters live:

for (int r = 1; r <= rows; r++) {         // outer: runs `rows` times
    for (int c = 1; c <= cols; c++) {     // inner: restarts at 1 every pass
        // this statement runs rows * cols times
    }
    // reached once per row, after the inner loop has finished
}

A return inside the inner loop leaves the method, which exits both loops at once. Anything you want done once per row goes between the inner loop's closing brace and the outer loop's closing brace.

Worked example · a table and a triangle

The table has a fixed inner bound; the triangle's inner bound is the outer variable. That one difference changes the shape of the output:

public void multiplicationTable(int n) {
    for (int r = 1; r <= n; r++) {
        String line = "";
        for (int c = 1; c <= n; c++) {
            int product = r * c;
            if (product < 10) {
                line = line + "  " + product;
            } else {
                line = line + " " + product;
            }
        }
        System.out.println(line);
    }
}

public void triangle(int n) {
    for (int r = 1; r <= n; r++) {
        String line = "";
        for (int c = 1; c <= r; c++) {
            line = line + "*";
        }
        System.out.println(line);
    }
}

Real output for n = 4:

  1  2  3  4
  2  4  6  8
  3  6  9 12
  4  8 12 16
*
**
***
****

Note where line is declared: inside the outer loop, so each row starts from an empty String. Declare it outside and every row would keep the previous row's characters.

Worked example · counting the passes of a triangular nest

How many times does the innermost statement run here, with n = 5?

int count = 0;
for (int i = 0; i < n; i++) {
    for (int j = 0; j < i; j++) {
        count++;
    }
}

The inner loop's bound is i, so each outer pass does more work than the last:

ij runs overinner passesrunning total
0nothing00
1011
20, 123
30, 1, 236
40, 1, 2, 3410

The run confirms it: total = 10, and n(n - 1)/2 gives 5 × 4 / 2 = 10 too. The first outer pass contributes nothing, because 0 < 0 is false: that empty first row is what most wrong answers miss.

Worked example · leaving both loops early

Search all pairs (i, j) from 1 to n for one whose product is target, and report how many pairs were examined:

public int pairsCheckedUntil(int n, int target) {
    int checked = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            checked++;
            if (i * j == target) {
                return checked;
            }
        }
    }
    return -1;
}
pairsCheckedUntil(6, 12) = 12
pairsCheckedUntil(6, 37) = -1

Exam tip: 12 is found at i = 2, j = 6 (the sixth pair of the second row, so the 12th pair overall), and the return abandons both loops immediately. Without a match the loops run all 36 pairs and fall through to the return -1;. The AP subset has no break, so return is how you leave a nest early.

Try it

How many times does count++ execute when n is 6? Answer by reasoning about the bound, then check against the formula.

int count = 0;
for (int i = 0; i < n; i++) {
    for (int j = i; j < n; j++) {
        count++;
    }
}
Show answer

21. The inner loop starts at i instead of 0, so it shrinks as i grows: 6 passes, then 5, 4, 3, 2, 1. That is 6 + 5 + 4 + 3 + 2 + 1 = 21, the sum n(n + 1)/2. The real run:

upperTriangle(6) = 21
n(n+1)/2 = 21

The previous example used n(n - 1)/2; the difference is one character. j = i includes the diagonal pair where j equals i; j < i excludes it. Check whether the innermost statement runs on the first outer pass: that tells you which formula you are looking at.

Lesson 2.12 · Unit 2 · CED topic 2.12

Informal run-time analysis

"How fast is this method?" is really the question "how many times does its innermost statement execute, as a function of the input size n?" You answer it by counting, not by timing: timings depend on the machine, but the count is a property of the code. AP Computer Science A asks for the count itself, in plain numbers, rather than big-O notation.

Three shapes cover nearly everything here. A single loop over the data runs about n times; a loop nested inside another over the same data runs about n² times; a loop that halves its variable each pass runs about log base 2 of n times. The gap between them is invisible at n = 4 and decisive at n = 1000.

Rule

Put a counter on the innermost statement and read the count:

for (int i = 0; i < n; i++) {        // about n executions
    count++;
}

for (int i = 0; i < n; i++) {        // about n * n executions
    for (int j = 0; j < n; j++) {
        count++;
    }
}

int x = n;                           // about log base 2 of n executions
while (x > 1) {
    x = x / 2;
    count++;
}

Only the innermost statement is counted, because it is the one that runs most often. Statements before or after the loop run a fixed number of times and do not change how the count grows.

Worked example · three methods, one table

Each method below returns its own execution count, so the table is the program's output rather than a prediction:

nA: single loopB: nested loopC: halving loop
44162
88643
16162564
10241024104857610

Read the table across the rows, not down the columns. Doubling n doubles A, multiplies B by four, and adds one to C, so by n = 1024, C has done 10 units of work where B has done more than a million. C is fastest for large n, and the justification the exam wants comes from the table, not a formula: "each time n doubles, C's count rises by only 1 while B's quadruples."

Worked example · why the halving loop counts what it counts

The loop in method C, spelled out for n = 16:

passx beforex aftercountx > 1
11681true
2842true
3423true
4214false

Four passes, and 16 is 2 to the fourth power. That is what "log base 2 of n" means here: the number of halvings that reach 1. The same argument is what makes binary search fast in Unit 4.

Exam tip: the question is usually worded "how many times is the body of the loop executed" or "which method is most efficient for large values of n". Count the innermost statement, and if the loop variable is multiplied or divided rather than added to, expect a count near a logarithm rather than near n.

Try it

How many times does the body of this loop execute when n is 100, and how would you justify the answer to a grader without running it?

for (int i = 1; i < n; i = i * 3) {
    count++;
}
Show answer

Five times. The real run:

    body runs with i = 1
    body runs with i = 3
    body runs with i = 9
    body runs with i = 27
    body runs with i = 81
iterations = 5

The justification: i takes the powers of 3 (1, 3, 9, 27, 81), and the next value, 243, fails i < 100. Five powers of 3 are below 100, so the body runs five times. Because i is multiplied rather than incremented, tripling the limit adds only one pass: n = 300 gives six. Change the update to i = i + 3 and the same loop runs 33 times: the difference between a count near its logarithm and a count near n.

Unit 2 review · 10 multiple-choice

Unit 2 review: selection and iteration

Ten exam-style multiple-choice questions in lesson order (boolean expressions and String comparison, if and nested if, compound conditions and De Morgan's laws, while and for loops, the accumulator patterns, String traversal, nested iteration and statement counting) with every printed value taken from a real run of the code.

Multiple choice

  1. A program stores a secret pass phrase and then assembles the phrase the user types, one piece at a time, before checking it.

    String secret = "ope";
    String typed = "op";
    typed = typed + "e";
    System.out.println((typed == secret) + " " + typed.equals(secret));

    What is printed as a result of executing the code segment?

    This assumes == compares the letters. For objects, == asks whether the two variables hold the same address, and the concatenation on line 3 built a brand-new String object rather than reusing the one named secret.

    Correct. The decisive line is line 3: joining "op" and "e" at run time produces a new object, so typed and secret are two different objects that happen to hold the same characters. == reports false on the addresses while equals reports true on the contents.

    equals compares character by character, and both Strings hold o, p, e in that order, so the second value cannot be false. Getting this half wrong usually means treating equals as a synonym for ==.

    This has the two comparisons backwards. == is the one that fails on separately built Strings, and equals is the one that succeeds, which is exactly why the exam insists on equals for every String comparison.

  2. int x = 5;
    if (x > 10);
    {
        x = x + 1;
    }
    System.out.println(x);

    What is the value of x after the code segment executes?

    5 is what you would get if the block belonged to the if. Read the semicolon at the end of line 2: it closes the if immediately, so the block below is no longer attached to any condition.

    Correct. The semicolon after if (x > 10) is a complete empty statement, so the if controls nothing at all. The braces that follow are just an ordinary block that always executes, and x goes from 5 to 6 even though x > 10 is false.

    11 would require the condition to be true and the body to add 6, neither of which happens. The body adds 1, and the condition is irrelevant because nothing depends on it.

    A semicolon there is legal Java, which is exactly why this bug is dangerous: the compiler accepts it silently and the program simply does the wrong thing. Removing the semicolon is the fix.

  3. public static double shipping(double pounds) {
        double cost = 0.0;
        if (pounds > 10.0) {
            cost = cost + 12.0;
        }
        if (pounds > 5.0) {
            cost = cost + 8.0;
        }
        else {
            cost = cost + 4.0;
        }
        return cost;
    }
    System.out.println(shipping(12.0) + " " + shipping(7.0) + " " + shipping(3.0));

    What is printed as a result of executing the code segment?

    This is the result the author probably wanted, and it is what an if / else if chain would produce. The two if statements here are independent, though, so the first one does not stop the second from being tested.

    Correct. The 12-pound order is charged twice: 12 is greater than 10, adding 12.0, and it is also greater than 5, adding 8.0, for 20.0. The 7-pound order fails the first test and passes the second, giving 8.0, and the 3-pound order falls to the else, giving 4.0. Writing else if on line 5 would make all three tiers exclusive.

    7 pounds never reaches the 12.0 charge, because 7.0 > 10.0 is false. The double charge in this method can only happen above 10 pounds, where both conditions are true at once.

    This mixes up which order is overcharged. 12 pounds triggers both additions and 7 pounds triggers only one, so the first value must be the larger of the two, not the smaller.

  4. String word = null;
    if (word != null && word.length() > 2) {
        System.out.println("long");
    }
    else {
        System.out.println("skipped");
    }

    What is printed as a result of executing the code segment?

    long requires both operands of && to be true, and the first one is already false because word holds null. A false left operand settles an && on its own.

    Correct. && short-circuits: once word != null evaluates to false, the whole condition is known to be false and word.length() is never evaluated. The else branch runs and prints skipped. This ordering is the standard null guard, and reversing the two operands would break it.

    Calling length() on null would indeed throw a NullPointerException, but that call never happens here. Short-circuiting is what protects it, which is the entire reason the null test is written first.

    An if with an else always takes exactly one of its two branches, so something is always printed. A false condition sends control to the else, it does not skip the statement altogether.

  5. The method below is being rewritten so that it returns its answer directly instead of returning true or false from an if-else.

    public static boolean isSafe(int x, int y) {
        if (!(x > 10 && y > 10)) {
            return true;
        }
        else {
            return false;
        }
    }
    public static boolean isSafe(int x, int y) {
        /* missing code */
    }

    Which replacement for /* missing code */ makes the method work as intended for all int values of x and y?

    Negating && gives ||, not &&. Testing both versions on every pair from 0 through 20 shows 220 disagreements, the first at x = 0, y = 11, where the original returns true and this replacement returns false.

    Correct. De Morgan's law turns !(x > 10 && y > 10) into !(x > 10) || !(y > 10), and negating each relational operator gives x <= 10 || y <= 10. Checked against the original on all 441 pairs from 0 through 20, it agrees every time, and returning the condition directly is always better than if (c) return true; else return false;.

    This drops the outer ! and changes && to ||, so it returns the exact opposite in most cases. At x = 0, y = 0 the original returns true while this returns false.

    This negates the wrong expression. !(x <= 10 && y <= 10) is equivalent to x > 10 || y > 10, which disagrees with the original on 221 of the 441 pairs tested, starting at x = 0, y = 0.

  6. int n = 4073;
    int count = 0;
    while (n > 0) {
        n = n / 10;
        count++;
    }

    How many times does the body of the loop execute?

    3 counts the divisions that leave something behind and forgets the last one. The fourth pass starts with n equal to 4, and 4 > 0 is true, so the body runs a fourth time before the condition finally fails.

    Correct. Integer division walks n through 4073, 407, 40, 4, then 0. The pass that decides the answer is the fourth: n is 4, the condition is still true, the body runs and makes n zero, and the fifth test fails. The result is the number of digits in 4073.

    5 counts one test too many. The pass that sets n to 0 is the fourth; the fifth time the condition is checked it is false, so no fifth body executes: count bodies, not tests.

    The loop stops when n reaches 0, and it does not need to go negative. Integer division of a positive number by 10 always shrinks it, so this loop is guaranteed to terminate; it is deleting the update line that would make it infinite.

  7. int sum = 0;
    for (int i = 3; i < 30; i = i + 3) {
        sum = sum + i;
    }
    System.out.println(sum);

    Which of the following code segments produces the same output as the code segment above?

    Changing < to <= lets i reach 30 and adds one extra term, so this prints 165 instead of 135. The bound in a for header must survive the translation unchanged.

    Updating before accumulating skips the first value and picks up an extra one at the end: this adds 6 through 30 and prints 162. In a for loop the update runs after the body, so the body must come first here too.

    Correct. A for loop is a while loop with its three parts moved: initialization before the loop, the same condition in the header, and the update as the last statement of the body. Both segments add 3, 6, 9, and on up to 27 and print 135.

    Moving the update outside the braces means i stays 3 forever and the condition never becomes false. Compiled and run, this segment never terminates and prints nothing at all.

  8. public static int largest(int low, int high) {
        int max = 0;
        for (int n = low; n <= high; n++) {
            int value = 12 - n * n;
            if (value > max) {
                max = value;
            }
        }
        return max;
    }

    What does the call largest(4, 7) return?

    12 is the constant in the formula, not a value the formula produces here. value would only be 12 if n were 0, and this call starts n at 4.

    Correct. The four values are 12 - 16, 12 - 25, 12 - 36 and 12 - 49, or -4, -13, -24 and -37. Every one of them fails value > max on the very first comparison, so max never moves off its starting 0 and the method returns a number the formula never produced. Start the running maximum at the first data value instead.

    -4 is the true largest value, and it is what the method should return, but it never gets stored, because -4 is not greater than the initial 0. This is the gap the question is testing.

    -37 is the smallest of the four values and the last one computed. The if only overwrites max with something larger, so the final value computed has no special status.

  9. String word = "class";
    String result = "";
    for (int i = 0; i < word.length(); i++) {
        result = word.substring(i, i + 1) + result;
    }
    System.out.println(result);

    What is printed as a result of executing the code segment?

    class is what you get from result = result + word.substring(i, i + 1);, with the accumulator on the left. The order of the two operands around + is the whole difference between copying a String and reversing it.

    Correct. Each new character is glued to the front of what has been built so far, so result grows c, lc, alc, salc, ssalc. The decisive pass is the last one, i = 4: the s at index 4 goes in front of salc and produces ssalc.

    This loses the first character, which would happen if the loop started at i = 1. The header starts at 0, and index 0 is the c, so all five characters are visited.

    The largest value i reaches is 4, so the last call is substring(4, 5), which is legal on a five-character String: the second argument may equal the length. The condition i < word.length() is what keeps it in bounds.

  10. int count = 0;
    for (int i = 0; i < 5; i++) {
        for (int j = 0; j < i; j++) {
            count++;
        }
    }
    System.out.println(count);

    How many times does the body of the inner loop execute?

    25 is 5 times 5, the count you get when the inner bound is the same constant as the outer one. Here the inner condition is j < i, so the inner loop runs a different number of times on each outer pass.

    20 would be 5 times 4, which treats the inner loop as running 4 times every pass. It runs 4 times only on the final pass, when i is 4.

    Correct. The inner loop runs 0 times when i is 0, then 1, 2, 3 and 4 times, and 0 + 1 + 2 + 3 + 4 is 10: the formula n * (n - 1) / 2 with n equal to 5. The first outer pass is the one people miss: an empty inner loop still counts as an outer pass. Even so, this shape grows like n squared, so doubling n roughly quadruples the count.

    15 is 1 + 2 + 3 + 4 + 5, which would be right if the inner condition were j <= i. With j < i the inner body runs exactly i times on each pass, so the sum starts at 0 and stops at 4.

Lesson 3.1 · Unit 3 · CED topics 3.1–3.2

Abstraction and program design

Until now you have used classes somebody else wrote. From here you write them, and the hard part is not the Java: it is deciding what the class is. Every class answers two questions: what must this thing remember, and what must it be able to do? The answers become the private instance variables and the public methods, in that order.

That is what abstraction buys you. A caller holding a lunch account should be able to deposit money and buy a meal without knowing whether the balance is stored in dollars or in cents. Every field you invent is data somebody has to keep correct, and design decisions reach real people: an assumption you bake into a method quietly decides who can use the program at all.

Rule

Design in this order (remember, start, do), and write the headers before any body:

public class ClassName {
    private Type fieldName;          // what the object must REMEMBER

    public ClassName(Type param) {   // how a new object STARTS
    }

    public Type methodName(Type p) { // what the object must DO
    }
}

Fields are private so that only this class's own methods can change them. Keep a field only if some method needs it later; anything a method can compute on demand is not state, it is a calculation.

Worked example · designing a lunch account

The description: Each student has a lunch account identified by a student ID. Money is added to the account by a parent. Buying a meal subtracts the price, but only if the balance covers it. A student approved for free lunch may always buy a meal, and the balance is untouched.

Pull the nouns for fields and the verbs for methods:

rememberstypewhy it must be stored
studentNameStringprinted on the kiosk; cannot be derived
idNumberintidentifies the account across the system
balancedoublechanges on every deposit and purchase
freeLunchbooleanchanges how buyMeal behaves

The verbs become four headers, written before a single body exists:

public LunchAccount(String studentName, int idNumber)
public double  getBalance()
public void    deposit(double amount)
public boolean buyMeal(double price)
public void    setFreeLunch(boolean status)

Notice that buyMeal returns boolean rather than void. The kiosk has to know whether the meal was actually paid for, and the only honest way to tell it is a return value.

Worked example · the design, compiled and driven

Fill the bodies in and drive the object from main. A deposit of 20.00, a 3.75 meal, an impossible 50.00 meal, then the same 50.00 meal after free lunch is switched on.

public boolean buyMeal(double price) {
    if (freeLunch) {
        return true;
    }
    if (balance >= price) {
        balance = balance - price;
        return true;
    }
    return false;
}

Compiled with javac and run, the driver prints:

balance 0.0
after deposit 20.0
buyMeal(3.75) true
balance 16.25
buyMeal(50.00) false
balance 16.25
buyMeal(50.00) true
balance 16.25

The last two lines are the design paying off: free lunch returns true and leaves the balance at 16.25, because the rule lives inside the class instead of inside every caller that ever sells a meal.

Try it

A school runs after-school tutoring slots. Each slot has a subject and a tutor, and holds a fixed number of students. A student may sign up while spots remain. List the fields with their types, write the method headers, then name one field you deliberately left out and one assumption in your design that would shut a real student out.

Show answer

Four fields and four headers cover it:

private String subject;
private String tutorName;
private int capacity;
private int filled;

public TutorSlot(String subject, String tutorName, int capacity)
public String  getSubject()
public int     spotsLeft()
public boolean signUp()

Left out on purpose: the list of student names. This class's job is to know whether a spot is free, and filled answers that in one int. Storing names here would duplicate data the roster already owns, and two copies of the same fact drift apart.

The assumption that excludes someone: one tutor, meeting after school. A student who rides the only bus home at dismissal can never take a slot, and nothing in the code will ever report that as a bug. Note too that spotsLeft is computed as capacity - filled rather than stored: a derived value is a method, not a field.

Lesson 3.2 · Unit 3 · CED topic 3.3

Anatomy of a class

A class file has a fixed shape, and the compiler expects each part in its place: the class header, then the private instance variables, then the constructor, then the methods. The class is the description. An object is one thing built from it by new. One Student class can produce three hundred student objects, and each one carries its own copy of every instance variable.

Instance variables are private for a reason worth stating plainly: a private field can only be changed by code inside the class, so every rule the class wants to enforce is enforceable. Make the field public and any line of any program can overwrite it, and the rule is gone. Fields you never assign are not garbage, either: Java gives every instance variable a default: 0, 0.0, false, or null for a reference.

Syntax

The parts, in the order the compiler expects to meet them:

public class Student {          // class header
    private String name;        // instance variable
    private int points;         // instance variable

    public Student(String startName) {   // constructor: same name, NO return type
        name = startName;
        points = 0;
    }

    public int getPoints() {    // accessor: returns a field, changes nothing
        return points;
    }

    public void addPoints(int earned) {  // mutator: changes a field, returns nothing
        points = points + earned;
    }
}

The file is named Student.java after the public class. Nothing outside the braces of a method runs on its own; all the work happens when a caller invokes the constructor or one of the methods.

Worked example · label every part

A real addPoints caps a single award at 10 points, which adds one more part: a local variable.

public void addPoints(int earned) {
    int capped = earned;
    if (capped > 10) {
        capped = 10;
    }
    points = points + capped;
}
textpartlives for
public class Studentclass headerthe whole file
private int points;instance variableas long as the object does
public Student(String startName)constructorone run per new
startNameparameterthat one constructor call
int cappedlocal variablethat one method call
getPoints()accessor,
addPoints(int)mutator,

Driving it with an award of 7 and then an illegal award of 25 prints:

points 0
points 7
points 17

The second award was capped at 10, so 7 + 10 is 17: the cap did its job.

Worked example · what a field starts as

This class has a constructor that assigns nothing at all. Printing the four fields immediately after new Blank() gives the defaults:

private String label;
private int count;
private double rate;
private boolean active;

public Blank() {
}

Real output:

label=null  count=0  rate=0.0  active=false

Exam tip: a local variable gets no such gift. Reading a local before you assign it is a compile-time error, variable x might not have been initialized, while reading an unassigned instance variable compiles and quietly hands you null or 0.

Try it

A caller writes s.points = -400; on a Student. Say exactly what the compiler does while points is private, then say what the program does once private is changed to public.

Show answer

While the field is private, the line never compiles. The real javac message:

Main.java:4: error: points has private access in Student
        s.points = -400;
         ^
1 error

Change one word to public int points; and the same program compiles and runs:

through the mutator: 10
straight at the field: -400

The first line shows addPoints(25) being capped at 10 as designed. The second line is the cost of a public field: the caller skipped the mutator entirely and left the object in a state the class swore was impossible. private is not secrecy: it is the guarantee that the rules in your methods are the only way in.

Lesson 3.3 · Unit 3 · CED topic 3.4

Constructors

A constructor is the method that runs once, at birth, to put an object into a legal starting state. It is written differently from every other method: its name is exactly the class name, and it has no return type at all, not even void. Write public void Rectangle(...) and you have not written a constructor; you have written an ordinary method that happens to be capitalized, and new Rectangle(3.0, 4.0) will not compile.

A class may have several constructors as long as their parameter lists differ: that is ordinary overloading. Java hands you a free no-argument constructor only if you write none yourself. The moment you write one constructor with parameters, the free one disappears, and that is the single most common surprise in this topic.

Syntax

Name, parameters, assignments. No return type on the header, no return in the body:

public class Rectangle {
    private double width;
    private double height;

    public Rectangle() {                 // no-argument constructor
        width = 1.0;
        height = 1.0;
    }

    public Rectangle(double startWidth, double startHeight) {   // overloaded
        width = startWidth;
        height = startHeight;
    }
}

Every constructor should leave every field with a sensible value. A field the constructor forgets keeps its default: 0.0 for a double, null for a String, and the bug surfaces much later, in somebody else's method.

Worked example · three objects, three starting states

Three new statements, one per constructor call, with the fields after each:

Rectangle a = new Rectangle();
Rectangle b = new Rectangle(3.0, 4.0);
Rectangle c = new Rectangle(2.5, 2.5);
statementconstructor chosenwidthheightgetArea()
new Rectangle()no-argument1.01.01.0
new Rectangle(3.0, 4.0)two-parameter3.04.012.0
new Rectangle(2.5, 2.5)two-parameter2.52.56.25

The real printed output:

a 1.0 x 1.0  area 1.0
b 3.0 x 4.0  area 12.0
c 2.5 x 2.5  area 6.25

The compiler picks the constructor by the number and types of arguments at the new, exactly the way it picks among overloaded methods.

Worked example · deleting the no-argument constructor

Remove public Rectangle() and recompile the same unchanged driver. javac answers:

Main.java:3: error: constructor Rectangle in class Rectangle cannot be applied to given types;
        Rectangle a = new Rectangle();
                      ^
  required: double,double
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error

Lines 4 and 5 still compile: only the no-argument call broke. Compare a class that declares no constructor at all:

public class Counter {
    private int count;

    public int getCount() {
        return count;
    }
}

new Counter() compiles and getCount() prints:

count 0

Exam tip: when a question shows a class with one parameterized constructor and a call with empty parentheses, that is a compile-time error, not a run-time one. The free constructor exists only in a class with no constructors at all.

Try it

Write both constructors for a Locker class with private int number; and private boolean assigned; from this specification, then say what new Locker() leaves in each field.

constructornumberassigned
Locker()0false
Locker(int startNumber)startNumberfalse
Show answer

Both constructors set both fields, even where the value repeats the default:

public Locker() {
    number = 0;
    assigned = false;
}

public Locker(int startNumber) {
    number = startNumber;
    assigned = false;
}

Building one of each and printing the fields gives:

new Locker()      number 0    assigned false
new Locker(214)   number 214  assigned false

Writing the redundant number = 0; is deliberate. It costs nothing, and it means a reader never has to remember the default table to know what a new Locker holds. Leaving assigned out of the second constructor would still print false, but only by accident, and an accident is not a specification.

Lesson 3.4 · Unit 3 · CED topic 3.5

Writing accessor and mutator methods

Once the fields are private, the methods are the whole interface. Two shapes cover most of them. An accessor returns information and changes nothing: its header has a return type and usually no parameters. A mutator changes the object and usually returns nothing: its header is void and it takes the new value as a parameter.

The interesting work happens inside a mutator. Because it is the only door into the field, it is also the only place a class can defend itself against a bad value. A thermostat that accepts 200 degrees is not a thermostat with a bug in it later; it is a thermostat with a bug in it now. Clamping the value, rather than storing it and hoping, is what makes the private field worth having.

Syntax

An accessor reports; a mutator validates, then stores. A private helper may do the validating:

public int getTarget() {          // accessor: return type, no parameters
    return target;
}

public void setTarget(int degrees) {   // mutator: void, one parameter
    target = clamp(degrees);           // one method of the class calling another
}

private int clamp(int degrees) {       // private: a tool for this class only
    // return degrees pulled inside the legal range
}

clamp is private because it is not part of the promise the class makes to the outside world. A caller who writes t.clamp(200) gets error: clamp(int) has private access in Thermostat.

Worked example · a thermostat that cannot be set to 200

The legal range is 50 through 85 degrees, inclusive at both ends. Anything below 50 becomes 50 and anything above 85 becomes 85: clamping, not rejecting.

private int clamp(int degrees) {
    if (degrees < 50) {
        return 50;
    }
    if (degrees > 85) {
        return 85;
    }
    return degrees;
}

Five calls on a thermostat that started at 68, with the field after each:

calldegreeswhich branchtarget after
setTarget(45)45below 5050
setTarget(50)50neither, boundary50
setTarget(68)68neither68
setTarget(85)85neither, boundary85
setTarget(90)90above 8585

The real run confirms every row:

start 68
setTarget(45) -> 50
setTarget(50) -> 50
setTarget(68) -> 68
setTarget(85) -> 85
setTarget(90) -> 85
Lab 2 ends at 85

Both boundary rows matter. Writing degrees <= 50 in the first test would still print 50, so the bug would hide; writing degrees >= 85 would too. Test the endpoints and one value on each side of them.

Worked example · the constructor reuses the mutator's rule

Nothing stops a caller from writing new Thermostat("Lab 2", 300). The fix is not a second copy of the clamping logic: it is the same helper, called from the constructor:

public Thermostat(String startRoom, int startTarget) {
    room = startRoom;
    target = clamp(startTarget);       // not  target = startTarget;
}

Exam tip: when Question 2 gives a value a legal range, the specification applies to every place that value enters the object: the constructor as well as the setter. Writing the rule once, in a private helper, is how you satisfy both without maintaining two copies of it.

Try it

Add public boolean nudge(int change) to Thermostat. It moves the target by change degrees, returns true if the new target landed inside the legal range untouched, and false if it had to be clamped. Then give the results for a thermostat at 84 receiving nudge(1) and then nudge(4).

Show answer

Compute the wanted value, clamp it, and compare the two before storing: the comparison is what tells you whether clamping happened:

public boolean nudge(int change) {
    int wanted = target + change;
    target = clamp(wanted);
    return wanted == target;
}

Compiled and run starting from a target of 84:

start 84
nudge(1) returned true   target 85
nudge(4) returned false  target 85

The first call wants 85, which is legal, so nothing is clamped and it reports true. The second wants 89, gets pulled back to 85, and reports false, and note that the target does not change, so a caller looping on nudge can stop the moment it sees false.

Lesson 3.5 · Unit 3 · CED topic 3.6

Passing and returning object references

Java always passes a copy of what the variable holds. For an int that is a copy of the number, so a method can never change the caller's int. For an object, what the variable holds is a reference, so the method gets a copy of the address: a second arrow pointing at the very same object. Call a mutator through that arrow and the caller's object changes for good.

The same fact runs in the other direction. If an accessor returns the reference stored in a private field, the caller now holds an arrow into your object's insides and can rearrange them without calling a single one of your methods. private did not stop it, because you handed the reference over yourself.

Rule

Mutating through a parameter reaches the caller. Reassigning a parameter does not.

public static void transfer(Account from, Account to, double amount) {
    from.withdraw(amount);   // reaches the caller's object
    to.deposit(amount);      // reaches the caller's object
}

public static void swap(Account x, Account y) {
    Account temp = x;
    x = y;                   // moves this method's OWN arrow
    y = temp;                // the caller never sees either move
}

The dot operator follows the arrow to the shared object. A plain = on the parameter only repoints the local arrow, and that arrow dies at the closing brace.

Worked example · a transfer that really moves money

Maya has 300.00 and Ravi has 50.00. Transfer 120.00 and print both balances again.

stepfrom points atto points atMaya's objectRavi's object
before the call, , 300.050.0
arguments copied inMaya's objectRavi's object300.050.0
from.withdraw(120.00)Maya's objectRavi's object180.050.0
to.deposit(120.00)Maya's objectRavi's object180.0170.0
method returnsgonegone180.0170.0

The real output:

before: Maya 300.0  Ravi 50.0
after:  Maya 180.0  Ravi 170.0

A void method returned nothing and still changed two objects permanently. That is only possible because both arrows survived the copy.

Worked example · the accessor that gives the class away

A Team keeps private ArrayList<String> roster;. This accessor looks harmless:

public ArrayList<String> getRoster() {
    return roster;              // hands out the field itself
}

A caller takes the returned list, removes an element and adds one of its own. Real output, with the team's own size() reported afterwards:

roster size 3
after the caller edits getRoster():  size 3  [Bo, Cy, Intruder]

The size is still 3, but Ana is gone and a name the class never approved is in the roster. Returning a defensive copy closes the hole: build a new list and fill it:

public ArrayList<String> getRosterCopy() {
    ArrayList<String> copy = new ArrayList<String>();
    for (int i = 0; i < roster.size(); i++) {
        copy.add(roster.get(i));
    }
    return copy;
}

The identical caller code now leaves the team untouched:

after the caller edits getRosterCopy(): size 3  [Ana, Bo, Cy]
the caller's own copy:                  [Bo, Cy, Intruder]

Exam tip: the design point on Question 2 is lost exactly here. Returning a private mutable field, or mutating a parameter the question never told you to mutate, both read as side effects beyond the specification.

Try it

A student writes swap(a, b) using the three-line temporary idiom on two Account parameters and is surprised that a and b are unchanged after the call. Predict the three printed lines and explain the difference between this and transfer.

Show answer

Printed before the call, inside the method, and after it returns:

before swap:  a is Maya  b is Ravi
inside swap:  x is Ravi   y is Maya
after swap:   a is Maya  b is Ravi

Inside the method the swap genuinely happened, to x and y, which are this method's own arrows. a and b back in main were never touched, because nothing was ever written through an arrow; the arrows themselves were moved.

transfer works because from.withdraw(...) follows the arrow and changes the object at the other end. The rule to carry into the exam: a method can change an object you pass it, but it can never change which object your variable refers to.

Lesson 3.6 · Unit 3 · CED topic 3.7

Class variables and class methods

An instance variable belongs to an object: build four tickets and there are four copies of holder. A static variable, a class variable, belongs to the class itself, and there is exactly one copy no matter how many objects exist, or whether any exist at all. That is what makes it the right tool for a fact about the whole class: how many objects have been created, or what the next ID number should be.

A static method follows the same logic. It runs on the class, not on an object, so there is no object for it to look at, and that single sentence explains every compile error in this topic. A static method cannot read an instance variable and cannot use this, because neither one exists until somebody supplies an object.

Syntax

One shared counter, one per-object copy of the value it handed out:

public class Ticket {
    private static int nextId = 1000;   // ONE copy, shared by the class

    private int id;                     // one copy per object
    private String holder;

    public Ticket(String startHolder) {
        holder = startHolder;
        id = nextId;      // take the current number
        nextId++;         // and move the shared counter on
    }

    public static void resetIds() {     // called as Ticket.resetIds()
        nextId = 1000;
    }
}

A constant is public static final: shared, readable by anyone, and impossible to reassign; public static final int MAX_OPTIONS = 4;. Constants are the one field the AP subset lets you make public.

Worked example · four tickets, one counter

Four new Ticket(...) statements. Watch the shared counter and each object's own id:

statementnextId beforeid given outnextId after
, 1000, 1000
new Ticket("Ana")100010001001
new Ticket("Bo")100110011002
new Ticket("Cy")100210021003
new Ticket("Di")100310031004

The real output, ending with a call to the static reset:

nextId before any ticket: 1000
a.getId() 1000   nextId 1001
b.getId() 1001   nextId 1002
c.getId() 1002   nextId 1003
d.getId() 1003   nextId 1004
a still has id 1000
after resetIds(): nextId 1000, a.getId() 1000

The last line is the point of the whole lesson. resetIds() moved the one shared counter back to 1000, and it did nothing whatever to Ana's ticket: her id was copied out of the counter four statements ago and has been her own private number ever since.

Worked example · why a static method cannot touch a field

Add one innocent-looking line to resetIds and recompile:

public static void resetIds() {
    nextId = 1000;
    id = 0;             // which ticket's id?
}

The real javac message:

Ticket.java:27: error: non-static variable id cannot be referenced from a static context
        id = 0;
        ^
1 error

Read the error as a question the compiler is asking: whose id? Ticket.resetIds() was called with no object, so there is no answer, and the compiler refuses rather than guessing. The same message appears the moment a static method writes this.

Try it

A Poll class has public static final int MAX_OPTIONS = 4;, a static totalVotes, an instance votes, and a vote() that increments both. Two polls are built, p.vote() runs twice and q.vote() once. Give the four printed values, and say why getVotes() cannot be static while getTotalVotes() can.

Show answer

Compiled and run:

p.getVotes() 2
q.getVotes() 1
Poll.getTotalVotes() 3
Poll.MAX_OPTIONS 4

Each poll counted only its own votes; totalVotes saw all three because there is only one of it. getVotes() must be an instance method because the answer depends on which poll you ask: its body reads an instance variable, and a static version of it would not compile. getTotalVotes() reads only the shared field, so it needs no object and is called on the class name.

Exam tip: when a multiple-choice item shows a counter that keeps climbing across objects, the field is static; when each object restarts from zero, it is an instance variable. Look at the declaration, not at the method names.

Lesson 3.7 · Unit 3 · CED topic 3.8

Scope and access

Scope is the region of code in which a name means something. Three kinds of variable live for three different lengths of time. An instance variable is declared inside the class but outside every method, and it lives as long as the object does. A parameter exists for one call. A local variable exists from its declaration to the closing brace of the block that holds it, and a loop body is a block, so a variable declared inside it is gone by the next line after the loop.

Access is a different question: who is allowed to say the name at all. private means only this class; public means anybody. The convention the exam expects is private fields and public methods, with a private helper method whenever a piece of logic is nobody else's business. When a parameter happens to share a field's name, the parameter wins inside that method, it shadows the field, and that is where an entire class of silent bugs comes from.

Rule

A name is visible from its declaration to the end of the block that declares it:

public class Profile {
    private String nickname;             // scope: the whole class, life: the object

    public int longestGap(int[] days) {  // days: scope and life = this call
        int best = 0;                    // best: scope = the method body
        for (int i = 1; i < days.length; i++) {   // i: scope = the for statement
            int gap = days[i] - days[i - 1];      // gap: scope = the loop body
            if (gap > best) {
                best = gap;
            }
        }
        return best;                     // gap and i no longer exist here
    }
}

Two locals may share a name in two different methods, or in two sibling blocks, because their scopes never overlap. A local may never share a name with another local that is still in scope.

Worked example · five identifiers, five scopes
identifierkindvisible instops existing when
nicknameinstance variableevery method of Profilethe object is discarded
daysparameterlongestGap onlythat call returns
bestlocal variablethe body of longestGapthat call returns
iloop control variablethe for header and its bodythe loop ends
gaplocal variableone pass of the loop bodythat pass ends

On the array {1, 3, 8, 9, 15} the gaps are 2, 5, 1 and 6, so the method returns 6. The real run prints longestGap: 6, and note that gap is created and destroyed four separate times to produce it.

Worked example · the setter that does nothing

The parameter is named after the field. The body looks perfectly reasonable:

private String nickname;

public void setNickname(String nickname) {
    nickname = nickname;          // parameter = parameter
}

Building a profile as "mya" and calling setNickname("Maya") prints:

before setNickname: mya
after  setNickname: mya

The field never changed. Inside that method nickname means the parameter at both ends of the =, so the statement copies the parameter onto itself and the field is never mentioned at all. Renaming the parameter fixes it, and the same driver then prints after setNickname: Maya:

public void setNickname(String newName) {
    nickname = newName;
}

Exam tip: javac reports nothing here, not even with -Xlint:all: the statement is legal Java, just useless. When an MCQ shows a setter and asks why the value never changes, check the parameter name against the field name first.

Try it

Somebody moves the two println calls out of the loop to print the last gap and the final index. Say exactly what the compiler reports for both lines.

for (int i = 1; i < days.length; i++) {
    int gap = days[i] - days[i - 1];
}
System.out.println(gap);
System.out.println(i);
Show answer

Two identical complaints: both names are out of scope by then:

Main.java:7: error: cannot find symbol
        System.out.println(gap);
                           ^
  symbol:   variable gap
  location: class Main
Main.java:8: error: cannot find symbol
        System.out.println(i);
                           ^
  symbol:   variable i
  location: class Main
2 errors

gap died at the closing brace of the loop body and i died with the for statement itself. cannot find symbol is the compiler saying the name is not in scope here: it is the same message you get for a misspelled variable, because to the compiler the two situations are identical.

To keep a value past the loop, declare it before the loop: int gap = 0; above the for, and the loop assigns to it instead of declaring its own.

Lesson 3.8 · Unit 3 · CED topic 3.9

The this keyword

Inside any instance method, this is a reference to the object the method was called on. When a caller writes c.setText("FINAL"), then for the duration of that call this is the object c refers to. You have been using it invisibly all along: writing text inside a method is shorthand for this.text.

Saying it out loud earns its keep in three places. It disambiguates a field from a parameter that shares its name, which lets you name the parameter after the thing it actually is. It can be passed to another method, so an object can hand itself over. And this(...) as the first statement of a constructor calls a different constructor of the same class, so the real work lives in one place.

Syntax

Three uses, all of them this:

public Thermostat(String room, int target) {
    this.room = room;              // field = parameter, same name, no ambiguity
    this.target = clamp(target);
}

public Thermostat(String room) {
    this(room, 68);                // call the other constructor; MUST be first
}

public String tag() {
    return Registry.describe(this);   // hand this whole object to another method
}

this(...) must be the very first statement in the constructor. One println above it and javac stops with error: call to this must be first statement in constructor.

Worked example · three setters, one call

Three labels are built holding "draft", and each gets setText("FINAL") through a different version of the setter.

public void setTextShadowed(String text) {
    text = text;              // both names mean the parameter
}

public void setTextRenamed(String newText) {
    text = newText;           // only one name in scope: the field
}

public void setTextThis(String text) {
    this.text = text;         // left side is the field, right side the parameter
}
versionleft side meansright side meansfield after the call
shadowedthe parameterthe parameterdraft
renamed parameterthe fieldthe parameterFINAL
this.textthe fieldthe parameterFINAL

The real output of all three:

shadowed: draft
renamed:  FINAL
this:     FINAL

Only the first one is broken, and it is the only one the compiler accepts without a murmur. That is why professionals reach for this.field = field;: it keeps the parameter's honest name and makes the shadowing harmless.

Worked example · one constructor doing the work for two

The one-argument constructor supplies a default target and then delegates. Every validation rule stays in a single place:

public Thermostat(String room) {
    this(room, 68);
}

Building new Thermostat("Lab 2") and new Thermostat("Gym", 300), then printing each through a method that passes this to Registry.describe:

one: Lab 2 @ 68
two: Gym @ 85

The Gym asked for 300 and got 85. The clamp runs in the two-argument constructor, and because the one-argument version delegates rather than duplicating, it can never drift out of step with it.

Try it

A student "fixes" the shadowed setter by writing this.text = this.text;. Say what the field holds after setText("FINAL") on a label built as "draft", then give the smallest edit that makes it correct and explain why the compiler accepts both versions.

Show answer

The field still holds draft. this.text = this.text; names the field on both sides, so it copies the field onto itself: the parameter is never read at all. Removing one this. from the right side fixes it:

public void setText(String text) {
    this.text = text;         // field  =  parameter
}

Compiled and run on a label built as "draft":

this.text = this.text;  ->  draft
this.text = text;       ->  FINAL

Exam tip: both versions are legal Java: assigning a variable to itself breaks no rule, so there is no error and no warning. On a "why does the value never change" question, read each side of the = and name what it refers to before you look at anything else.

Lesson 3.9 · Unit 3 · CED topics 3.3–3.9

Designing a complete class, end to end

Question 2 of the free-response section always asks for one complete class, and it hands you a specification table of the members it wants. The workflow that earns the points is mechanical, and doing it in order is most of the battle: declare the private fields, write the constructor, write each method in the order the table lists it, then walk every row of the examples table through the code you just wrote.

That last step is the one students skip and the one that finds the bug. The examples table is not decoration: it is the scoring guide showing you its hand. If your class reproduces every row, the correctness points are already in your pocket.

Rule

Read the table top to bottom and write the class top to bottom:

public class CoffeeCard {
    private String owner;        // 1. what the table says it remembers
    private int punches;
    private int freeDrinks;

    public CoffeeCard(String owner) {    // 2. the constructor, every field set
    }

    public void buy() {                  // 3. each method, in the table's order
    }

    public int getFreeDrinks() {
    }

    public boolean redeem() {
    }

    public String toString() {           // 4. exact text, to the character
    }
}

The class must stand alone and compile: no main, and no printing inside any method. A method asked to return a value that prints it instead earns nothing.

Worked example · the punch card

The specification: one purchase adds one punch; every fifth punch becomes one free drink and the punch count returns to zero; redeem spends one free drink and reports whether it could; toString returns the owner, a space, the punches, a slash, 5, a comma and space, the free-drink count, a space and the word free.

public void buy() {
    punches++;
    if (punches == 5) {
        freeDrinks++;
        punches = 0;
    }
}

public boolean redeem() {
    if (freeDrinks > 0) {
        freeDrinks--;
        return true;
    }
    return false;
}

public String toString() {
    return owner + " " + punches + "/5, " + freeDrinks + " free";
}

Six purchases and two redemptions, straight from a real run:

new card        Maya 0/5, 0 free
after buy 1     Maya 1/5, 0 free
after buy 2     Maya 2/5, 0 free
after buy 3     Maya 3/5, 0 free
after buy 4     Maya 4/5, 0 free
after buy 5     Maya 0/5, 1 free
after buy 6     Maya 1/5, 1 free
redeem() returns true   Maya 1/5, 0 free
redeem() returns false   Maya 1/5, 0 free

Every row of the specification is confirmed, including the one students forget: the second redeem has nothing to spend, so it changes nothing and returns false rather than driving freeDrinks to −1.

Worked example · the missing line that costs a point

Delete punches = 0; from buy and rerun the identical driver:

afterpunches (correct)free (correct)punches (bug)free (bug)
buy 44040
buy 50151
buy 61161
buy 1002101

The buggy version really prints:

after buy 5     Maya 5/5, 1 free
after buy 6     Maya 6/5, 1 free

Exam tip: Maya 6/5 is the tell: a card advertising six punches out of five. And because the test is punches == 5, the tenth purchase never triggers a second free drink at all. One deleted line breaks two different scoring rows, which is exactly why you check the examples table against the code rather than against your intentions.

Try it

The specification grows by one row: public int punchesToNextFree() returns how many more purchases are needed before the next free drink. Write it, then give the value it returns after 0, 3 and 6 purchases.

Show answer

It is a derived value, so compute it: do not add a fourth field:

public int punchesToNextFree() {
    return 5 - punches;
}

Compiled and run on a fresh card:

after 0 buys  Maya 0/5, 0 free   need 5
after 3 buys  Maya 3/5, 0 free   need 2
after 6 buys  Maya 1/5, 1 free   need 4

The third row is the interesting one. Six purchases left the card at one punch, because the fifth purchase reset the counter, so four more are needed, not −1, which is what a stored countdown field would have drifted to. A value you can compute from the fields you already have is a method, and keeping it that way is part of the design point.

Lesson 3.10 · Unit 3 · CED topic 3.10 (inheritance strand)

Inheritance: writing a subclass

Unit 3 closes with the inheritance strand of the revised framework. Confirm the exact topic numbers against the published CED before release; the content below is the strand as the course outline describes it.

Inheritance is for an is-a relationship. A student ticket is a ticket, it has everything a ticket has and adds a discount, so StudentTicket extends Ticket is the right shape. Write extends and the subclass immediately has every public method of the superclass without copying a line.

What it does not get is access to the superclass's private fields. private means "this class only", and a subclass is a different class. The subclass reaches the data the same way any outsider does, through the inherited public accessors, and it hands the superclass its share of the construction work with super(...).

Syntax

extends in the header, super(...) first in the constructor:

public class StudentTicket extends Ticket {
    private int percentOff;                 // only the NEW field is declared here

    public StudentTicket(String event, double basePrice, int percentOff) {
        super(event, basePrice);            // MUST be the first statement
        this.percentOff = percentOff;
    }

    public double getPrice() {              // overrides Ticket's version
        double full = super.getPrice();     // ask the superclass for its answer
        return full * (100 - percentOff) / 100.0;
    }
}

super(...) calls a superclass constructor; super.method() calls the superclass's version of a method. They are different tools that share a keyword.

Worked example · a discounted ticket

Ticket holds private String event and private double basePrice, with getEvent(), getPrice() and toString(). StudentTicket adds a percentage off, clamped into 0 through 50, and rounds the result to cents with the AP idiom.

public double getPrice() {
    double full = super.getPrice();
    double cut = full * (100 - percentOff) / 100.0;
    return (int) (cut * 100 + 0.5) / 100.0;
}

Three tickets on a 40.00 recital seat, run for real:

25 off  -> stored 25  price 30.0
60 off  -> stored 50  price 20.0
-5 off  -> stored 0  price 40.0
event through the inherited accessor: Recital
Recital $30.0 (25% student)

getEvent() was never written in StudentTicket: it was inherited whole. The last line is the overridden toString(), which calls super.toString() and appends its own detail rather than rebuilding it.

Worked example · three ways to break the subclass

Each of these is one edit away from the working class, and each has its own message.

1. super(...) deleted. Java tries to call a no-argument superclass constructor that does not exist:

StudentTicket.java:4: error: constructor Ticket in class Ticket cannot be applied to given types;
    public StudentTicket(String event, double basePrice, int percentOff) {
                                                                         ^
  required: String,double
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error

2. super(...) moved below the clamping. Legal statements, illegal order:

StudentTicket.java:11: error: call to super must be first statement in constructor
        super(event, basePrice);
             ^
1 error

3. The subclass reads basePrice directly instead of calling super.getPrice():

StudentTicket.java:20: error: basePrice has private access in Ticket
        double full = basePrice;
                      ^
1 error

Exam tip: error 3 is the one that costs points on Question 2. The superclass fields are listed right there in the question, which makes them look available: they are not. Every value you need from the superclass comes through a public accessor or through super.method().

Try it

Write SeniorTicket extends Ticket from this specification: a constructor SeniorTicket(String event, double basePrice, double discount) storing a flat dollar discount clamped into 0 through the base price, an accessor getDiscount(), and an overridden getPrice() returning the base price minus the discount, rounded to cents. Then give the price for a 40.00 seat with a 12.50 discount and with a 55.00 discount.

Show answer

Clamp against the constructor's own basePrice parameter (a local name, not the superclass's private field) then delegate to super.getPrice():

public class SeniorTicket extends Ticket {
    private double discount;

    public SeniorTicket(String event, double basePrice, double discount) {
        super(event, basePrice);
        if (discount < 0) {
            discount = 0;
        }
        if (discount > basePrice) {
            discount = basePrice;
        }
        this.discount = discount;
    }

    public double getDiscount() {
        return discount;
    }

    public double getPrice() {
        return (int) ((super.getPrice() - discount) * 100 + 0.5) / 100.0;
    }
}

Compiled and run:

discount 12.50 -> stored 12.5  price 27.5
discount 55.00 -> stored 40.0  price 0.0

The second row is the clamp working: a 55.00 discount on a 40.00 seat is stored as 40.00 and the ticket is free, not −15.00. Note that super.getPrice() is the only way to reach the base price once the constructor has finished: the parameter is gone by then.

Lesson 3.11 · Unit 3 · CED topic 3.10 (inheritance strand)

Overriding, polymorphism, and the Object superclass

This lesson completes the inheritance strand that closes Unit 3 in the revised framework. Confirm the exact topic numbers against the published CED before release.

An override is a subclass method with the same signature (same name, same parameter list) as one it inherited. From then on, an object of the subclass runs its own version. The power comes from what you may do with the result: a superclass reference is allowed to refer to a subclass object, so Ticket t may hold a StudentTicket, and one loop over an array of Ticket can drive objects of several different classes.

Two types are in play, and keeping them apart is the whole topic. The declared type decides which methods you are allowed to call: the compiler checks that, before the program runs. The actual type of the object decides which version runs: the JVM decides that, at run time. Every class also inherits from Object, which is where toString() and equals() come from.

Rule

A superclass reference may hold a subclass object; the object's own method runs.

Ticket[] cart = new Ticket[4];
cart[0] = new Ticket("Recital", 40.00);
cart[1] = new StudentTicket("Recital", 40.00, 25);   // legal: a StudentTicket IS a Ticket

for (int i = 0; i < cart.length; i++) {
    System.out.println(cart[i].getPrice());   // declared Ticket, runs the object's version
}

// cart[1].getPercentOff();                  // will NOT compile: Ticket has no such method
((StudentTicket) cart[1]).getPercentOff();    // cast tells the compiler what it really is

Override toString() and every System.out.println(obj) and every "" + obj uses it automatically. Leave it alone and you get Object's version, which prints the class name, an at sign and a hash: a real run of a class with no toString printed Plain@2a139a55.

Worked example · one loop, two classes

Four tickets in one array, printed and totalled by a single loop:

indexdeclared typeactual objectgetPrice() runsvalue
0TicketTicketTicket's40.0
1TicketStudentTicket 25%StudentTicket's30.0
2TicketTicketTicket's18.5
3TicketStudentTicket 10%StudentTicket's16.65

The real output:

0: Recital $40.0   getPrice() = 40.0
1: Recital $30.0 (25% student)   getPrice() = 30.0
2: Playoff $18.5   getPrice() = 18.5
3: Playoff $16.65 (10% student)   getPrice() = 16.65
total 105.15

Nothing in the loop mentions StudentTicket. Rows 1 and 3 are discounted because the objects themselves decide, and 18.50 less 10% is 16.65 after the rounding idiom.

Worked example · what Object gives you

toString() and equals() are inherited by every class. toString is the one you override; Object's equals compares references, so two separately built tickets with identical fields are not equal:

Ticket x = new Ticket("Recital", 40.00);
Ticket y = new Ticket("Recital", 40.00);
System.out.println("x.equals(y) " + x.equals(y));
System.out.println("x.equals(x) " + x.equals(x));

Real output:

x.equals(y) false
x.equals(x) true

Exam tip: unless a question shows you an equals written inside the class, equals means == on references: the same trap as comparing Strings, one level up. "Same contents" is only true equality when somebody wrote the method that says so.

Try it

Using the same array, say exactly what the compiler does with System.out.println(cart[1].getPercentOff());, write the line that works, and then say what the four getPrice() values would be if StudentTicket did not override getPrice() at all.

Show answer

The declared type is Ticket, and Ticket has no such method:

Main.java:6: error: cannot find symbol
        System.out.println(cart[1].getPercentOff());
                                  ^
  symbol:   method getPercentOff()
  location: class Ticket
1 error

A cast promises the compiler that the object really is a StudentTicket, and the parentheses matter: cast first, then call:

System.out.println(((StudentTicket) cart[1]).getPercentOff());   // prints 25

Deleting the override and rerunning the identical loop gives:

0: Recital $40.0   getPrice() = 40.0
1: Recital $40.0 (25% student)   getPrice() = 40.0
2: Playoff $18.5   getPrice() = 18.5
3: Playoff $18.5 (10% student)   getPrice() = 18.5
total 117.0

Every price is now the undiscounted one, because the inherited Ticket.getPrice() is the only version that exists. Row 1 still says 25% student, the toString override is untouched, which is the clearest possible picture of what an override does and does not change.

Unit 3 review · 10 multiple-choice

Unit 3 review: Class Creation

Ten exam-style questions on a code stimulus, running from class design and constructors through references, static members, scope and this to the inheritance strand: click an option to see why it is right or wrong, and read the stimulus line by line before you choose.

Multiple choice

  1. public class Student {
        private String name;
        private int points;
    
        public Student(String startName) {
            name = startName;
            points = 0;
        }
    
        public int getPoints() {
            return points;
        }
    
        public void addPoints(int earned) {
            int capped = earned;
            if (capped > 10) {
                capped = 10;
            }
            points = points + capped;
        }
    }
    // inside main, in a different class
    Student s = new Student("Ana");
    s.addPoints(7);
    s.addPoints(25);
    s.points = 40;
    System.out.println(s.getPoints());

    What is the result of compiling and running the code segment in main?

    17 is what this segment would print with the fourth line deleted: addPoints(7) adds 7, and addPoints(25) is pulled down to 10 by the local variable capped, so the field holds 7 + 10. Nothing runs, though, because the program is rejected before it is ever executed.

    40 assumes s.points = 40; is a legal statement. It is not: points is declared private, so only code written inside Student may name it, and main lives in another class.

    32 is 7 + 25, the total you would get if addPoints stored earned directly. It does not, the cap is enforced through capped, and in any case the segment never compiles.

    Correct. javac reports error: points has private access in Student and stops. That is exactly what private buys you: the cap inside addPoints is the only way points can ever enter the object, so no caller can leave it in a state the class forbids.

  2. public class Locker {
        private int number;
        private boolean assigned;
    
        public Locker(int startNumber) {
            number = startNumber;
            assigned = false;
        }
    
        public int getNumber() {
            return number;
        }
    }

    Which of the following statements, placed in main, causes a compile-time error?

    This matches the only constructor the class has, one int argument, so it compiles and builds a locker numbered 214. The constructor's name and parameter list are the whole contract.

    Creating an array of objects runs no constructor at all. It makes three slots holding null, which compiles cleanly; the error would come later, at run time, if you called a method through one of those null slots.

    Correct. Java supplies a free no-argument constructor only to a class that declares no constructor whatsoever. Locker declares one, so the free one is gone and javac answers constructor Locker in class Locker cannot be applied to given types; required: int, found: no arguments.

    Zero is an ordinary int argument, so this compiles and stores 0 in number. A value being unusual is not the same as a value being illegal: only a mutator or constructor that validates could reject it.

  3. public class Thermostat {
        private String room;
        private int target;
    
        public Thermostat(String startRoom, int startTarget) {
            room = startRoom;
            target = clamp(startTarget);
        }
    
        public int getTarget() {
            return target;
        }
    
        public void setTarget(int degrees) {
            target = clamp(degrees);
        }
    
        private int clamp(int degrees) {
            if (degrees < 50) {
                return 50;
            }
            if (degrees > 85) {
                return 85;
            }
            return degrees;
        }
    }
    Thermostat t = new Thermostat("Lab 2", 90);
    System.out.print(t.getTarget() + " ");
    t.setTarget(45);
    System.out.print(t.getTarget() + " ");
    t.setTarget(70);
    System.out.println(t.getTarget());

    What is printed as a result of executing the code segment?

    Correct. The first value decides it: new Thermostat("Lab 2", 90) passes 90 through clamp, which returns 85 because 90 is above the ceiling. setTarget(45) is then pulled up to the floor of 50, and 70 is inside the legal range so it is stored untouched.

    This is the output of a class that stores whatever it is handed. Both the constructor and the mutator call clamp, so 90 and 45 never reach the field unchanged: that is the whole reason the field is private.

    90 for the first value assumes the constructor assigns startTarget directly. It does not: it calls the same helper the mutator calls. Writing the rule once, in a private helper, is what keeps the two entry points from drifting apart.

    45 for the second value assumes the clamping happens only at construction. setTarget also calls clamp, and 45 is below 50, so the field becomes 50. A range stated in a specification applies at every door into the object.

  4. public static void grow(Rectangle r, double factor) {
        r.setWidth(r.getWidth() * factor);
    }
    
    public static void replace(Rectangle r) {
        r = new Rectangle(1.0, 1.0);
    }
    
    public static void main(String[] args) {
        Rectangle a = new Rectangle(3.0, 4.0);
        Rectangle b = a;
        grow(a, 2.0);
        replace(a);
        System.out.println(a.getWidth() + " " + b.getWidth());
    }

    What is printed as a result of executing main?

    This treats b = a; as copying the rectangle. It copies the reference, so a and b are two arrows pointing at one object, and a change made through either arrow is visible through the other.

    3.0 for both says grow could not reach the caller's object. It could: r is a copy of the reference, and r.setWidth(...) follows that reference to the very same rectangle a names.

    Correct. The decisive call is grow, which sets the shared object's width to 6.0 through the copied reference. replace then only repoints its own parameter r at a new rectangle, and that arrow dies at the closing brace, so both a and b still report 6.0.

    1.0 assumes replace can change which object a refers to. A method can change an object you hand it, but it can never change which object your variable points at: assigning to a parameter moves only the method's own arrow.

  5. public class Ticket {
        private static int nextId = 1000;
    
        private int id;
        private String holder;
    
        public Ticket(String startHolder) {
            holder = startHolder;
            id = nextId;
            nextId++;
        }
    
        public int getId() {
            return id;
        }
    
        public static void resetIds() {
            nextId = 1000;
        }
    }
    Ticket a = new Ticket("Ana");
    Ticket b = new Ticket("Bo");
    Ticket.resetIds();
    Ticket c = new Ticket("Cy");
    System.out.println(a.getId() + " " + b.getId() + " " + c.getId());

    What is printed as a result of executing the code segment?

    This is the output with the resetIds() call deleted. The call is there, and it assigns 1000 to the one shared counter, so the third ticket cannot receive 1002.

    All three equal would mean each object has its own copy of nextId. It is declared static, so there is exactly one copy for the whole class, and the first two tickets took different numbers out of it.

    Correct. The third value is where the answer is decided: resetIds() moved the shared counter back to 1000, so Cy's ticket takes 1000 again. It did nothing to Ana's ticket, whose id was copied out of the counter two statements earlier and has been her own instance variable ever since.

    These are the numbers you get if the constructor increments before it assigns. It assigns first (id = nextId;) and then advances the counter, so the first ticket issued gets the starting value 1000.

  6. public class Profile {
        public int longestGap(int[] days) {                    // line 2
            for (int i = 1; i < days.length; i++) {            // line 3
                int best = 0;                                  // line 4
                int gap = days[i] - days[i - 1];               // line 5
                if (gap > best) {                              // line 6
                    best = gap;                                // line 7
                }
            }
            return best;                                       // line 10
        }
    }

    The method is intended to return the largest gap between consecutive values of days, but the class does not compile. Which change makes it compile and work as intended for every array of length at least 2?

    Swapping > for >= changes only which of two equal gaps wins, and the class still will not compile. The error is not about the comparison at all.

    Returning gap fails twice over: gap is also declared inside the loop body, so it is out of scope at line 10, and even in scope it would report the last gap rather than the largest one.

    Seeding the running maximum with an element is sometimes the right habit, but the declaration is still inside the loop, so it resets on every pass and is still out of scope at line 10. The class does not compile either way.

    Correct. javac says cannot find symbol: variable best at line 10, because a local variable declared inside the loop body dies at that body's closing brace. Declared above the loop, best is initialized once, survives every pass, and is still in scope at the return, on {1, 3, 8, 9, 15} the fixed method returns 6.

  7. public class Label {
        private String text;
    
        public Label(String text) {
            this.text = text;
        }
    
        public String getText() {
            return text;
        }
    
        public void setText(String text) {
            text = text;                    // line 13
        }
    }
    Label c = new Label("draft");
    c.setText("FINAL");
    System.out.println(c.getText());        // prints draft

    Which of the following replacements for line 13 makes setText work as intended, so that the segment prints FINAL?

    Both sides now name the field, so the statement copies the field onto itself and the parameter is never read. It compiles without a murmur and still prints draft: assigning a variable to itself breaks no rule of Java.

    Correct. Inside the method the bare name text means the parameter, because a parameter shadows a field of the same name; this.text always means the field of the object the method was called on. So the left side is the field, the right side is the argument, and the run prints FINAL.

    This is the original bug pointed the other way: it copies the field into the parameter, which is discarded when the method returns. The field is never written, so the output stays draft.

    Qualifying a field with the class name only works for a static field, and text is an instance variable. This one does not even compile: non-static variable text cannot be referenced from a static context.

  8. public class PunchPass {
        private String owner;
        private int punches;
        private int freeRides;
    
        public PunchPass(String owner) {
            this.owner = owner;
            punches = 0;
            freeRides = 0;
        }
    
        public void buy() {
            /* to be implemented */
        }
    
        public String toString() {
            return owner + " " + punches + "/3, " + freeRides + " free";
        }
    }

    One purchase adds one punch; every third punch becomes one free ride and the punch count returns to zero. Which implementation of buy() makes a new pass print Maya 1/3, 2 free after seven calls, and behave correctly for any number of calls?

    Correct. The fifth and the sixth calls decide it: the sixth call takes the counter to 3, which awards the second free ride and resets the counter, so the seventh call leaves exactly one punch on a pass holding two free rides; Maya 1/3, 2 free.

    The free-ride count is right, but the punch count is not: without punches = 0; the counter keeps climbing, so seven calls print Maya 7/3, 2 free. The specification says the count returns to zero, and toString makes the omission visible.

    Nothing ever resets the counter, so from the third purchase onward every call awards another free ride. Seven calls print Maya 7/3, 5 free, and a pass advertising seven punches out of three is the tell that the reset is missing.

    Assigning freeRides = 1; instead of incrementing it throws away every ride already earned, so the count can never exceed one. Seven calls print Maya 1/3, 1 free, which is right on the punches and wrong on the rides.

  9. public class Ticket {
        private String event;
        private double basePrice;
    
        public Ticket(String event, double basePrice) {
            this.event = event;
            this.basePrice = basePrice;
        }
    
        public String getEvent() {
            return event;
        }
    
        public double getPrice() {
            return basePrice;
        }
    }
    
    public class StudentTicket extends Ticket {
        private int percentOff;
    
        public StudentTicket(String event, double basePrice, int percentOff) {
            super(event, basePrice);
            this.percentOff = percentOff;
        }
    
        public double getPrice() {
            /* line 9 of the subclass */
        }
    }

    Which of the following replacements for line 9 makes new StudentTicket("Recital", 40.0, 25).getPrice() return 30.0?

    A subclass is a different class, so the superclass's private fields are invisible to it no matter how plainly they are printed in the question. javac stops with basePrice has private access in Ticket.

    Correct. super.getPrice() asks the superclass for the value it alone can reach, and 40.0 times 75 divided by 100.0 is 30.0. Note that the division is by 100.0 and not 100, so no integer division rounds the answer down.

    An unqualified getPrice() inside getPrice calls this same overriding method again, with no base case to stop it. The program compiles and then dies at run time with StackOverflowError.

    This computes the discount rather than the price: 40.0 times 25 divided by 100.0 is 10.0, the money saved. Subtracting it from the base price, or multiplying by 100 - percentOff, gives the amount actually charged.

  10. // Ticket declares getEvent(), getPrice() and toString().
    // StudentTicket extends Ticket, overrides getPrice() and toString(),
    // and adds public int getPercentOff().
    
    Ticket t = new StudentTicket("Recital", 40.0, 25);

    Which of the following statements, placed immediately after the declaration above, fails to compile?

    Every Ticket has getEvent(), so the compiler is satisfied and the run prints Recital. The subclass inherited that method whole, without a line being rewritten.

    This compiles because Ticket declares getPrice(), and at run time the object's own overriding version executes, so it prints 30.0 rather than 40.0. That split is the point of polymorphism: the declared type decides what may be called, the actual object decides what runs.

    Correct. The declared type of t is Ticket, and Ticket declares no getPercentOff, so the compiler refuses with cannot find symbol: method getPercentOff(), location: variable t of type Ticket, even though the object really is a StudentTicket.

    The cast promises the compiler that the object is a StudentTicket, which it is, so this compiles and prints 25. Note the extra parentheses: you must cast first and call second, or the cast would apply to the result of the call.

Lesson 4.1 · Unit 4 · CED topic 4.1

Ethical and social issues around collecting data

Unit 4 is about programs that hold a data set rather than a handful of loose variables. The moment a program holds data about people, two questions come with it: what did it collect, and how long does it keep it? Neither is a legal technicality. A field you store is a field that can leak, be subpoenaed, be sold with the company, or be wrong about someone who has no way to correct it.

The exam asks about this in plain language: identify what personal data a program collects, say who could be harmed, and name a change that reduces the harm. The answers are concrete: drop a field, shorten a retention window, ask before collecting, or fix the sample the program was tuned on.

Rule

Data minimization: store a field only if the program's stated purpose fails without it. Consent: the person knows what is collected and agrees to that use, not to every later use. Retention: every stored field needs a date it is deleted.

public class AttendanceRecord {
    private String studentId;     // needed: which student this row is about
    private String date;          // needed: which day
    private boolean present;      // needed: the fact being recorded

    // private String homeAddress;   // not needed to take attendance
    // private String photo;         // not needed to take attendance
}

The two commented lines are the whole lesson. Attendance is answerable without them, so collecting them creates risk that buys the program nothing.

Worked example · auditing an attendance app field by field

A school app records a check-in. Go through its fields and ask, for each one, what breaks if the field is removed.

fieldneeded to take attendance?decision
studentIdyes, the record has to name someonekeep
dateyes, attendance is per daykeep
presentyes, this is the fact itselfkeep
homeAddressno, the office already has itstop collecting
photono (used only to auto-check-in a facecollect with consent, or drop
gpsAtCheckInno) the door scanner knows the doorstop collecting

Retention matters as much as collection: keeping present for the school year is defensible; keeping four years of check-in photos is not.

Worked example · what an unbalanced data set does

Suppose the photo check-in feature was tuned on 1000 pictures. This program reports each group's share of that set next to how often the feature then misidentified that group in testing.

public static void report(String group, int samples, int errors, int total) {
    double share = samples * 100.0 / total;
    double errorRate = errors * 100.0 / samples;
    System.out.println(group + ": " + samples + " photos, " + round1(share)
        + "% of the training set, wrong " + round1(errorRate) + "% of the time");
}

Called with 820, 150 and 30 photos and 25, 12 and 9 errors, it prints:

total samples = 1000
Group A: 820 photos, 82.0% of the training set, wrong 3.0% of the time
Group B: 150 photos, 15.0% of the training set, wrong 8.0% of the time
Group C: 30 photos, 3.0% of the training set, wrong 30.0% of the time

The overall error rate is a comfortable 4.6%, and it hides everything. Group C is 3% of the data and wrong ten times as often as Group A. Exam tip: when a question hands you a skewed data set, the expected answer names a specific harm to a specific group, not "the program is biased."

Try it

A district plans to flag students for a tutoring program using a model trained on last year's records. Ninety percent of those records come from two large schools; the four small rural schools contributed almost none. Name one specific harm that follows, and one concrete change to the collection or the program that reduces it.

Show answer

Harm: students at the rural schools are flagged by a rule fitted to a population they are not part of, so the model under-flags them. Students who need tutoring do not get an invitation, and because the program never sees them improve, next year's data is skewed the same way: the error compounds.

Change: collect a proportional sample from every school before retraining, and report accuracy per school rather than one district-wide number, so a 30% miss rate at one school cannot hide inside a 95% average.

Lesson 4.2 · Unit 4 · CED topics 4.2–4.3

Data sets and creating arrays

Seven quiz scores in seven variables named quiz1 through quiz7 is not seven times the work: it is seven times the work forever, because no loop can touch them. An array is one name for a fixed-size run of same-typed boxes, numbered from 0. One name means one loop, and one loop means the average of seven scores is the same code as the average of seven hundred.

Two facts about arrays cause most of the pain. The size is fixed the instant the array is created and can never change. And the valid indexes run from 0 to length - 1, so an array of length 7 has no element 7.

Syntax

Two ways to create one: empty at a chosen size, or filled from a list:

int[] scores = new int[7];               // 7 boxes, each holding the default value
int[] quiz   = {88, 92, 75, 100, 64, 92, 81};   // size comes from the list

scores[0] = 88;                          // store into a box
int first = quiz[0];                     // read a box
int n = quiz.length;                     // a field, no parentheses

length is a field, so it is quiz.length, never quiz.length(), which is a compile-time error. Every box of a new array starts at that type's default: 0 for int, 0.0 for double, false for boolean, and null for any object type.

Worked example · a gradebook, built both ways

Build the same seven scores with each form and print what the array knows.

int[] scores = new int[7];
scores[0] = 88;
scores[1] = 92;
scores[2] = 75;
scores[3] = 100;
scores[4] = 64;
scores[5] = 92;
scores[6] = 81;

int[] quiz = {88, 92, 75, 100, 64, 92, 81};

System.out.println("scores.length = " + scores.length);
System.out.println("quiz.length   = " + quiz.length);
System.out.println("scores[0] = " + scores[0] + "   quiz[0] = " + quiz[0]);
System.out.println("scores[6] = " + scores[6] + "   quiz[6] = " + quiz[6]);
System.out.println("last element = " + quiz[quiz.length - 1]);

Output:

scores.length = 7
quiz.length   = 7
scores[0] = 88   quiz[0] = 88
scores[6] = 81   quiz[6] = 81
last element = 81

Printing element 0 of four freshly created arrays shows the defaults:

int default     0
double default  0.0
boolean default false
String default  null

That last one matters later: a new String[3] is three empty slots, not three empty strings, and calling a method on one of them throws.

Worked example · which index expressions are legal

Given int[] temps = {58, 63, 71, 69, 74, 66}; (length 6, indexes 0–5), evaluate six expressions.

expressionindex it computesresult
temps[0]058
temps[2]271
temps[5]566
temps[temps.length - 1]566
temps[temps.length]6throws
temps[-1]−1throws

The two failures, as the JVM reports them:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 6 out of bounds for length 6
    at Main.main(Main.java:8)
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 6
    at Main.main(Main.java:4)

Exam tip: the message tells you both numbers: the index that was tried and the length. Index 6 out of bounds for length 6 is the signature of forgetting the - 1, and it is a run-time error, so the program prints everything before it and then dies.

Try it

Declare an array of the five double values 3.5, 7.25, 1.0, 9.75 and 4.5 using an initializer list. Then state what each of these prints or does: data.length, data[1], data[data.length - 2], and data[5].

Show answer

The list form sets the size for you:

double[] data = {3.5, 7.25, 1.0, 9.75, 4.5};

data.length is 5. data[1] is 7.25: index 1 is the second element. data[data.length - 2] is data[3], which is 9.75. And data[5] throws ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5, because the last legal index is 4.

Lesson 4.3 · Unit 4 · CED topic 4.4

Traversing an array

A traversal visits every element once. Java gives you two loops for it, and choosing between them is a real decision, not a style preference. The indexed for loop hands you i, so it can read the slot, write the slot, compare a slot to its neighbor, skip slots, or run backwards. The enhanced for loop hands you a copy of each value in order, which is shorter to write and impossible to get off by one.

Everything the enhanced loop cannot do follows from that one word, copy. It cannot change the array, it does not know the index, and it always starts at the beginning and runs to the end.

Syntax

The two traversals, over the same array:

for (int i = 0; i < scores.length; i++) {      // indexed: i is the position
    System.out.println(i + ": " + scores[i]);
}

for (int s : scores) {                         // enhanced: s is a copy of the value
    System.out.println(s);
}

Read the indexed header as three jobs: start at 0, keep going while i < length, step by one. Use <, never <=. The enhanced header reads "for each int s in scores" and needs the element type on the left.

Worked example · all of them, then every other one

With int[] scores = {88, 92, 75, 100, 64, 92, 81, 79};, print every element, then every element at an even index, then prove the enhanced loop cannot write back.

for (int i = 0; i < scores.length; i++) {
    System.out.print(scores[i] + " ");
}

for (int i = 0; i < scores.length; i += 2) {
    System.out.print(scores[i] + " ");
}

for (int s : scores) {
    s = 0;                      // assigns to the copy, not to the array
}

Output, with a label printed before each line:

every element:  88 92 75 100 64 92 81 79
every other:    88 75 64 81
enhanced for:   88 92 75 100 64 92 81 79
after s = 0:    88 92 75 100 64 92 81 79 

The fourth line is the point. Setting s = 0 eight times changed nothing, because s is a fresh copy on every pass. To zero the array you need scores[i] = 0; inside an indexed loop.

Worked example · four headers, one array of length 8

Each header loops over the same eight-element array. Which are correct?

headerindexes visitedverdict
for (int i = 0; i < scores.length; i++)0–7correct
for (int i = 1; i < scores.length; i++)1–7silently skips index 0
for (int i = 0; i <= scores.length; i++)0–8throws at i = 8
for (int i = 0; i < scores.length - 1; i++)0–6silently skips index 7

Headers B and D print short but do not crash; C runs eight clean passes and then dies:

i = 6  ->  81
i = 7  ->  79
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 8
    at Main.main(Main.java:5)

Exam tip: the silent failures are the dangerous ones. When a question says a method "does not work as intended for all values", check both ends of the range first: starting at 1, or stopping at length - 1, is the most common planted bug. Partial traversals are fine when you mean them: comparing neighbours needs for (int i = 0; i < a.length - 1; i++) so that a[i + 1] stays in bounds.

Try it

Write a method countAbove that returns how many elements of an int[] are strictly greater than a given cutoff. Then say which of the two loop styles you used and why the other one would also have worked here.

Show answer

Counting only reads, so the enhanced loop is the cleaner choice:

public static int countAbove(int[] values, int cutoff) {
    int count = 0;
    for (int v : values) {
        if (v > cutoff) {
            count++;
        }
    }
    return count;
}

On {88, 92, 75, 100, 64, 92, 81, 79} with a cutoff of 80 it returns 5: the values 88, 92, 100, 92 and 81. An indexed loop works equally well because the method never modifies an element and never needs to know where a match was. The moment the question asks for the index of the first match, the enhanced loop is out.

Lesson 4.4 · Unit 4 · CED topic 4.5

The array algorithms the exam reuses

There is a short list of things people actually do to an array, and the exam draws from it over and over: sum and average, count the elements matching a condition, find the maximum or minimum and the index where it occurs, decide whether the array is sorted, shift the elements, and reverse in place. Learn the shapes once and you stop inventing them under time pressure.

One structural decision runs through all of them. An array parameter is a reference, so a method that writes into it changes the caller's array and usually returns void. A method that must leave the original alone builds and returns a new array instead.

Rule

Every one of these is the same skeleton: set up an accumulator, traverse, return.

public static int sum(int[] values) {
    int total = 0;                       // accumulator, before the loop
    for (int v : values) {
        total += v;                      // update on every pass
    }
    return total;                        // after the loop, not inside it
}

Three rules come out of it: declare the accumulator before the loop or it resets every pass; put return after the loop or it quits on pass one; and start the accumulator at the identity for the job: 0 for a sum or a count, and values[0] (never 0) for a maximum.

Worked example · a Stats class

Three methods, including the empty-array guard the exam expects on an average.

public static double average(int[] values) {
    if (values.length == 0) {
        return 0.0;                      // guard: no division by zero
    }
    return (double) sum(values) / values.length;
}

public static int indexOfMax(int[] values) {
    int best = values[0];
    int bestIndex = 0;
    for (int i = 1; i < values.length; i++) {
        if (values[i] > best) {
            best = values[i];
            bestIndex = i;
        }
    }
    return bestIndex;
}

Run on {12, 47, 31, 47, 9}:

sum        = 146
average    = 29.2
indexOfMax = 1
average of an empty array = 0.0

The cast in average is not decoration. Without it, 146 / 5 is integer division and the method returns 29.0.

Worked example · tracing indexOfMax, and the tie

Follow best and bestIndex through {12, 47, 31, 47, 9}. The loop starts at i = 1 because element 0 is already the running champion.

ivalues[i]values[i] > best?bestbestIndex
before, , 120
147true471
231false471
347false471
49false471

Row 3 is the tie. Because the test is > and not >=, a later equal value never displaces the champion, so the method returns the first index holding the maximum: 1. Change that one character to >= and the same array returns 3: verified by running both.

Exam tip: free-response questions state the tie rule ("if more than one element is the largest, return the smallest such index"). Match the operator to the sentence: > keeps the first, >= keeps the last.

Worked example · mutate, or return a new array

Two methods that both double every element, but only one leaves the caller's array intact.

public static int[] doubled(int[] values) {       // builds a new array
    int[] result = new int[values.length];
    for (int i = 0; i < values.length; i++) {
        result[i] = values[i] * 2;
    }
    return result;
}

public static void scaleInPlace(int[] values, int factor) {   // writes into the caller's array
    for (int i = 0; i < values.length; i++) {
        values[i] = values[i] * factor;
    }
}

Starting from int[] b = {3, 5, 8};:

b 3 5 8    c 6 10 16
b 6 10 16  after scaleInPlace

After doubled(b), b is untouched. After scaleInPlace(b, 2), b itself has changed: the method returned nothing and still did something permanent.

Try it

Write reverse(int[] values), which reverses the array in place and returns nothing. Then state the array after calling it on {12, 47, 31, 47, 9}, and explain why the loop must stop halfway.

Show answer

Swap the ends inward, using a temporary so neither value is lost:

public static void reverse(int[] values) {
    for (int i = 0; i < values.length / 2; i++) {
        int temp = values[i];
        values[i] = values[values.length - 1 - i];
        values[values.length - 1 - i] = temp;
    }
}

Running it:

before  12 47 31 47 9
after   9 47 31 47 12 

Each pass fixes two positions, so a full-length loop would swap every pair back and hand you the original array. With an odd length, integer division gives 5 / 2 = 2 passes and the middle element correctly stays put.

Lesson 4.5 · Unit 4 · CED topic 4.6

Reading a data set from a text file

Real data sets do not arrive as initializer lists. They arrive as a file of lines, one record per line, fields separated by a comma. Reading one is three jobs stacked: open the file, pull off one line at a time, and cut each line into fields you can convert to numbers.

The tool is a Scanner pointed at a File instead of at System.in. Everything else you already know: indexOf and substring do the cutting, and Integer.parseInt and Double.parseDouble turn the resulting text into numbers.

Syntax

The loop that reads a whole file, line by line:

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public static void load(String fileName) throws IOException {
    Scanner in = new Scanner(new File(fileName));
    while (in.hasNextLine()) {
        String line = in.nextLine();
        // cut the line into fields here
    }
    in.close();
}

hasNextLine() asks "is there another line?" and nextLine() consumes it. The throws IOException on the header is not optional: opening a file is a checked exception, and leaving it off is a compile-time error.

Main.java:6: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
        Scanner in = new Scanner(new File("stations.txt"));
                     ^
1 error
Worked example · station,reading into parallel arrays

The file stations.txt holds a name, a comma, and a reading, plus one blank line:

Elm St,18.4
Oak Ave,22.1

Pine Rd,31.7
Cedar Ln,27.5

Two parallel arrays hold the result: names[i] and levels[i] describe the same station because they share an index.

while (in.hasNextLine()) {
    String line = in.nextLine();
    int comma = line.indexOf(",");
    if (comma >= 0) {
        names[count] = line.substring(0, comma);
        levels[count] = Double.parseDouble(line.substring(comma + 1));
        count++;
    }
}

With a print statement added inside the loop, the run produces:

pass 1: comma at 6, name = Elm St, level = 18.4
pass 2: comma at 7, name = Oak Ave, level = 22.1
skipped: no comma on this line
pass 3: comma at 7, name = Pine Rd, level = 31.7
pass 4: comma at 8, name = Cedar Ln, level = 27.5
stations read = 4
mean level = 24.93
line readcommasubstring(0, comma)substring(comma + 1)count after
Elm St,18.46Elm St18.41
Oak Ave,22.17Oak Ave22.12
(blank)−1skippedskipped2
Pine Rd,31.77Pine Rd31.73
Cedar Ln,27.58Cedar Ln27.54

Note that count only advances on a good line, so no gap ever appears in the arrays, and the mean uses count, not levels.length, which is the capacity, not the number of readings.

Worked example · what the blank line does without the guard

Delete the if (comma >= 0) test and the third line detonates:

name = Elm St
name = Oak Ave
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [0, -1) out of bounds for length 0
    at java.base/java.lang.String.substring(String.java:2823)
    at Main.main(Main.java:11)

On a blank line indexOf(",") returns −1, so the call becomes "".substring(0, -1). The message names the range it was asked for, [0, -1), and the length it had, 0. Guarding on comma >= 0 skips blank lines and malformed lines in one test.

Exam tip: know which parser you need. Integer.parseInt("0042") + 1 prints 43, and Double.parseDouble("18.4") + 1 prints 19.4, but Integer.parseInt("18.4") throws NumberFormatException: For input string: "18.4": a decimal point is not an integer.

Try it

The file now uses a colon instead of a comma, and some lines are comments starting with #. Rewrite the body of the loop so that comment lines and blank lines are both skipped, and say what indexOf returns for the line # sensor swapped 3/14.

Show answer

One test handles both: a comment has no colon, and neither does a blank line.

String line = in.nextLine();
int colon = line.indexOf(":");
if (colon >= 0) {
    names[count] = line.substring(0, colon);
    levels[count] = Double.parseDouble(line.substring(colon + 1));
    count++;
}

For # sensor swapped 3/14, indexOf(":") returns −1, so the guard is false and the line is dropped. If a comment could contain a colon, add an explicit test first, if (line.length() > 0 && !line.substring(0, 1).equals("#")): remembering that a single character is substring(0, 1) compared with equals.

Lesson 4.6 · Unit 4 · CED topic 4.7

Wrapper classes and autoboxing

An ArrayList stores objects, and int is not an object. That is the entire reason the wrapper classes exist. Integer is an object whose only job is to hold one int; Double does the same for a double. Wrapped, a number can live in a list.

Java hides the wrapping. Autoboxing converts an int to an Integer wherever an object is expected, and unboxing goes the other way wherever a primitive is expected. You write list.add(88) and int x = list.get(0); and Java inserts both conversions. This is a convenience until the day == tells you the truth about objects instead of the answer you wanted about numbers.

Rule

The type parameter of an ArrayList must be a class, so int becomes Integer:

ArrayList<Integer> scores = new ArrayList<Integer>();
scores.add(88);                  // autobox: int 88 becomes an Integer
int first = scores.get(0);       // unbox: Integer becomes int
int total = 0;
for (int s : scores) {           // unboxes each element into s
    total += s;
}

Integer.MIN_VALUE is −2147483648 and Integer.MAX_VALUE is 2147483647; Integer.parseInt(s) turns text into an int. And compare two wrapper objects with equals, never ==.

Worked example · summing an ArrayList of Integer into an int

Four scores go in as int literals and come back out as int values.

ArrayList<Integer> scores = new ArrayList<Integer>();
scores.add(88);
scores.add(92);
scores.add(75);
scores.add(100);

int total = 0;
for (int s : scores) {
    total += s;
}
System.out.println("scores = " + scores);
System.out.println("total  = " + total);
System.out.println("mean   = " + (double) total / scores.size());

Output:

scores = [88, 92, 75, 100]
total  = 355
mean   = 88.75

Printing the list directly gives the bracketed form: that is ArrayList's toString, and it is fine to rely on it when a question says "print the list".

Worked example · box, unbox, or neither

For each statement, name the conversion Java inserts and the resulting type.

statementconversiontype of the result
Integer boxed = 42;autoboxInteger
int plain = boxed;unboxint
scores.add(88);autoboxInteger stored in the list
int s = scores.get(0);unboxint
int n = Integer.parseInt("407");neither: parsing, not boxingint

The last row catches people. Integer.parseInt reads text and produces an int; no Integer object is involved. Running it, parsed + 1 prints 408.

Worked example · why == on Integer is a trap

Four wrapper objects, two comparisons, one surprising result.

Integer big1 = 1000;
Integer big2 = 1000;
Integer small1 = 100;
Integer small2 = 100;

Real output:

big1 == big2       false
big1.equals(big2)  true
small1 == small2   true
small1.equals(small2) true

== on two objects asks "are these the same object?" Java keeps a cache of small Integer objects (−128 to 127), so both 100 boxes point at one cached object and == happens to be true. 1000 is outside the cache, so two separate objects are made and == is false.

Exam tip: this is the same rule as == on String. Never let the accident of caching decide a test: compare wrapper objects with equals, or unbox first (big1.intValue() == big2.intValue() is true) and compare int values.

Try it

Write a method largest(ArrayList<Integer> list) that returns the largest value as an int, returning Integer.MIN_VALUE when the list is empty. Then say why initializing best to 0 would be a bug.

Show answer

Start from the sentinel, then let the traversal unbox each element for you:

public static int largest(ArrayList<Integer> list) {
    int best = Integer.MIN_VALUE;
    for (int v : list) {
        if (v > best) {
            best = v;
        }
    }
    return best;
}

On [88, 92, 75, 100] it returns 100, and on an empty list it returns −2147483648. Initializing best to 0 breaks on a list of negative values such as [-9, -4, -30]: every element fails the > test and the method reports 0, a number that is not in the list at all. Integer.MIN_VALUE is guaranteed to lose to any real int.

Lesson 4.7 · Unit 4 · CED topics 4.8–4.9

ArrayList: a list that changes size

An array's size is frozen at creation, which is fine for seven quiz scores and useless for a waiting list that grows all afternoon. An ArrayList is a list of object references that resizes itself: add makes it one longer, remove makes it one shorter, and size() always reports the current count.

The price is that you must think about shifting. Inserting at an index pushes every later element one slot right; removing at an index pulls every later element one slot left. Almost every ArrayList bug on the exam is really a shifting bug.

Syntax

Declare it, then use the six methods the exam expects you to know:

ArrayList<String> names = new ArrayList<String>();

names.add("Ana");            // append at the end
names.add(1, "Bo");          // insert at index 1, shifting the rest right
String s = names.get(0);     // read index 0
names.set(0, "Amir");        // replace index 0, returns the old value
String gone = names.remove(2);   // delete index 2, shifting the rest left
int n = names.size();        // a method, with parentheses

size() is a method, unlike an array's length field. The type in the angle brackets must be a class, so a list of whole numbers is ArrayList<Integer>. Writing ArrayList<int> gives error: unexpected type / required: reference / found: int.

Worked example · a waiting list

Start with four names and run six operations, printing the list and its size after each.

ArrayList<String> waiting = new ArrayList<String>();
waiting.add("Ana");
waiting.add("Ben");
waiting.add("Cleo");
waiting.add("Dev");

waiting.add("Eli");
waiting.add(1, "Bo");
String old = waiting.set(0, "Amir");
String gone = waiting.remove(2);
System.out.println(waiting.get(2));
waiting.add(waiting.size(), "Fay");

Output:

start  ->  [Ana, Ben, Cleo, Dev]  size 4
add("Eli")  ->  [Ana, Ben, Cleo, Dev, Eli]  size 5
add(1, "Bo")  ->  [Ana, Bo, Ben, Cleo, Dev, Eli]  size 6
set(0, "Amir") returned Ana  ->  [Amir, Bo, Ben, Cleo, Dev, Eli]  size 6
remove(2) returned Ben  ->  [Amir, Bo, Cleo, Dev, Eli]  size 5
get(2) = Cleo   (list unchanged)
add(size(), "Fay")  ->  [Amir, Bo, Cleo, Dev, Eli, Fay]  size 6
Worked example · the same six operations as a table

Read the size column first: it tells you instantly which calls resize the list.

callreturnslist afterwardssize
, , [Ana, Ben, Cleo, Dev]4
add("Eli")true[Ana, Ben, Cleo, Dev, Eli]5
add(1, "Bo")void[Ana, Bo, Ben, Cleo, Dev, Eli]6
set(0, "Amir")Ana[Amir, Bo, Ben, Cleo, Dev, Eli]6
remove(2)Ben[Amir, Bo, Cleo, Dev, Eli]5
get(2)Cleo[Amir, Bo, Cleo, Dev, Eli]5
add(size(), "Fay")true[Amir, Bo, Cleo, Dev, Eli, Fay]6

The shift is visible in rows 3 and 5. add(1, "Bo") moved Ben from index 1 to index 2, and remove(2) then deleted Ben and dragged Cleo, Dev and Eli each one place left, which is why the very next get(2) answers Cleo, not Ben.

Exam tip: set never changes size(); add and remove always do. And the legal index for add(int, E) runs up to size(): appending at size() is allowed, while get(size()) is out of bounds.

Try it

Starting from [10, 20, 30, 40] in an ArrayList<Integer>, give the list and its size after each of these four calls, in order: nums.remove(0), nums.add(1, 25), nums.set(2, 99), nums.add(nums.size(), 50).

Show answer

Track the shift after every call rather than reading the original positions:

calllist afterwardssize
remove(0)[20, 30, 40]3
add(1, 25)[20, 25, 30, 40]4
set(2, 99)[20, 25, 99, 40]4
add(size(), 50)[20, 25, 99, 40, 50]5

The third call is the one to check. Index 2 holds 30 only after the insertion shifted it there, so set(2, 99) overwrites 30, not 40. Note also that remove(0) on an ArrayList<Integer> means remove at index 0: the value removed here happens to be 10.

Lesson 4.8 · Unit 4 · CED topic 4.10

Traversing an ArrayList, and the removal trap

Traversing a list looks like traversing an array with two spellings changed: size() instead of length, and get(i) instead of [i]. The enhanced for loop works too, and it is the right choice whenever you are only reading.

The trap is removal. A list shifts when you delete from it, so the element that was at index i + 1 slides down into index i, and a loop that has just done i++ sails straight past it. This one bug appears on the exam every single year.

Rule

Three traversals, and when each is legal:

for (int i = 0; i < list.size(); i++) {     // indexed: reading, writing, or removing
    System.out.println(list.get(i));
}

for (String s : list) {                     // enhanced: reading only
    System.out.println(s);
}

for (int i = list.size() - 1; i >= 0; i--) {    // backward: the safe way to remove
    if (list.get(i).equals("")) {
        list.remove(i);
    }
}

Never add to or remove from a list inside an enhanced for loop. Java notices and throws ConcurrentModificationException at run time.

Worked example · the skip, traced

Remove every empty string from [red, "", "", blue, "", green] with the obvious forward loop.

public static void removeEmpty(ArrayList<String> list) {
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i).equals("")) {
            list.remove(i);              // BUG: i++ still happens
        }
    }
}
ii < size()?element seenlist after the passsize
00 < 6red[red, , , blue, , green]6
11 < 6(empty): removed[red, , blue, , green]5
22 < 5blue[red, , blue, , green]5
33 < 5(empty): removed[red, , blue, green]4
44 < 4 is false: [red, , blue, green]4

Result:

result [red, , blue, green]  size 4

Look at the row for i = 2. The removal at index 1 slid the second empty string down into index 1, and the loop was already heading for index 2, so that element was never examined. One empty string survives. Two empty strings in a row is exactly the input a grader uses to catch this.

Worked example · the two standard fixes

Fix one: walk backward, so a shift only affects indexes you have already passed:

for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).equals("")) {
        list.remove(i);
    }
}

Fix two: go forward, but only advance when nothing was removed:

int i = 0;
while (i < list.size()) {
    if (list.get(i).equals("")) {
        list.remove(i);
    } else {
        i++;
    }
}

Both run on the same six-element list and both print:

result [red, blue, green]  size 3

And the enhanced for loop, asked to do the same job, refuses:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
    at Main.main(Main.java:10)

Exam tip: when a free-response question says "remove all …", write the backward loop first and you have removed the whole risk. Note also that the condition i < list.size() is re-evaluated every pass: the loop notices the shrinking list, which is why it ends early rather than going out of bounds.

Try it

Write removeBelow(ArrayList<Integer> nums, int cutoff), which deletes every element strictly less than cutoff. Then give the result of calling it on [8, 3, 2, 11, 4, 7] with a cutoff of 5, and say what the buggy forward version would have produced instead.

Show answer

Backward, because the method removes:

public static void removeBelow(ArrayList<Integer> nums, int cutoff) {
    for (int i = nums.size() - 1; i >= 0; i--) {
        if (nums.get(i) < cutoff) {
            nums.remove(i);
        }
    }
}

The correct result, verified by running it:

[8, 11, 7]

The buggy forward loop leaves [8, 2, 11, 7]. It removes the 3 at index 1, which slides 2 into index 1, then jumps to index 2 and never looks at the 2 at all. The adjacent pair 3 and 2 is the tell.

Lesson 4.9 · Unit 4 · CED topic 4.11

ArrayList algorithms for data analysis

Question 3 of the free-response section is always the same shape: a class holds a private ArrayList of objects, and you write methods that walk that list, call the objects' accessor methods, and report something. Filter into a new list. Count and average the matches. Find the maximum by a field. Insert into a list you are keeping sorted. Remove everything matching a condition.

Two habits earn points on every one of them. Compare String fields with equals, never ==. And guard the empty case before you divide, returning the sentinel the question names: usually −1.0.

Rule

The Question 3 skeleton: a private list, and a traversal that calls accessors.

public class AirQualityLog {
    private ArrayList<Reading> readings;    // each Reading has getStation() and getLevel()

    public double averageFor(String station) {
        double total = 0.0;
        int count = 0;
        for (Reading r : readings) {
            if (r.getStation().equals(station)) {
                total += r.getLevel();
                count++;
            }
        }
        if (count == 0) {
            return -1.0;                    // the guard the question asks for
        }
        return total / count;
    }
}

The enhanced loop is correct here because the method only reads. Use an indexed loop the moment you need the position, and a backward indexed loop the moment you remove.

Worked example · averaging one station, and filtering

Five readings go into the log; three of them are from Elm St.

log.add(new Reading("Elm St", 18.4));
log.add(new Reading("Oak Ave", 22.1));
log.add(new Reading("Elm St", 25.0));
log.add(new Reading("Pine Rd", 31.7));
log.add(new Reading("Elm St", 20.1));

Output:

averageFor("Elm St")  = 21.166666666666668
rounded              = 21.17
averageFor("Maple")  = -1.0
above(21.0)          = [Oak Ave 22.1, Elm St 25.0, Pine Rd 31.7]

above(21.0) is the filter pattern: create an empty ArrayList<Reading> before the loop, add each match, return it at the end. The original list is untouched. Rounding to cents uses the subset idiom (int) (x * 100 + 0.5) / 100.0, which turns 21.166666666666668 into 21.17.

Worked example · insertInOrder, traced

The list is kept sorted ascending by level. Walk forward while the element you are looking at belongs before the new one, then insert at the index where you stopped.

public void insertInOrder(Reading r) {
    int i = 0;
    while (i < readings.size() && readings.get(i).getLevel() <= r.getLevel()) {
        i++;
    }
    readings.add(i, r);
}

Insert 22.1 into [12.0, 18.4, 25.0, 31.7]:

ii < size()?readings.get(i) levellevel <= 22.1?action
0true12.0truei becomes 1
1true18.4truei becomes 2
2true25.0falsestop, add(2, 22.1)
add(2, 22.1)  ->  [12.0, 18.4, 22.1, 25.0, 31.7]

Both edge cases fall out of the same two-part condition. A value below everything:

i = 0  list level 12.0 <= 5.0  false -> stop
add(0, 5.0)  ->  [5.0, 12.0, 18.4, 22.1, 25.0, 31.7]

And a value above everything:

i = 6  i == size -> stop
add(6, 40.0)  ->  [5.0, 12.0, 18.4, 22.1, 25.0, 31.7, 40.0]

Exam tip: the order of the two tests is not negotiable. i < readings.size() must come first, because && short-circuits: if i has reached size(), the second half is never evaluated and get(i) never runs out of bounds. Swap them and the "larger than everything" case throws.

Try it

Write highestAt(String station) for AirQualityLog. It returns the largest level recorded at that station, or −1.0 if the station has no readings. Use the five readings above and state what highestAt("Elm St") and highestAt("Maple") return.

Show answer

Do not seed best with readings.get(0): that reading may be from another station. Track whether anything matched instead:

public double highestAt(String station) {
    double best = -1.0;
    int count = 0;
    for (Reading r : readings) {
        if (r.getStation().equals(station)) {
            if (count == 0 || r.getLevel() > best) {
                best = r.getLevel();
            }
            count++;
        }
    }
    if (count == 0) {
        return -1.0;
    }
    return best;
}

highestAt("Elm St") returns 25.0: the Elm St readings are 18.4, 25.0 and 20.1, and highestAt("Maple") returns −1.0. The count == 0 test makes the very first match become the champion whatever its value, so the method is still correct if every level is negative.

Lesson 4.10 · Unit 4 · CED topic 4.12

Creating and accessing a 2D array

A seating chart, a game board, a spreadsheet of monthly rainfall by city: all of them are a grid, and a grid in Java is a 2D array. The honest description is that int[][] is an array whose elements are themselves int[] arrays: an array of rows.

That description answers the two questions students get wrong. grid.length is the number of rows, because the outer array holds rows. grid[0].length is the number of columns, because it is the length of one row. And every subscript is grid[row][column], in that order, always.

Syntax

Two ways to build one, exactly parallel to the 1D forms:

int[][] chart = new int[3][4];         // 3 rows, 4 columns, every cell 0

int[][] sold = {{0, 0, 1, 0},          // row 0
                {1, 1, 0, 0},          // row 1
                {0, 0, 0, 1}};         // row 2

chart[1][0] = 1;                       // row 1, column 0
int seat = sold[2][3];                 // row 2, column 3
int rows = sold.length;                // 3
int cols = sold[0].length;             // 4

On this exam every 2D array is rectangular, every row has the same length, and is stored in row-major order, so the row index comes first in the brackets and in every loop you write.

Worked example · a 3-by-4 seating chart, built both ways

Mark four sold seats in a chart created empty, then build the identical chart from a nested initializer list, and print both.

int[][] chart = new int[3][4];
chart[0][2] = 1;
chart[1][0] = 1;
chart[1][1] = 1;
chart[2][3] = 1;

int[][] sold = {{0, 0, 1, 0},
                {1, 1, 0, 0},
                {0, 0, 0, 1}};

Output:

chart, built with new int[3][4]:
0 0 1 0
1 1 0 0
0 0 0 1
sold, built with an initializer list:
0 0 1 0
1 1 0 0
0 0 0 1
rows    = 3
columns = 4
seats   = 12

The two are the same grid. Reach for new when the size is known but the contents arrive later, and for the literal when you are writing a small fixed grid into a test.

Worked example · reading positions, and five index expressions

Take this grid of readings:

int[][] grid = {{14, 22, 35, 41},
                {53, 60, 78, 82},
                {91, 17, 26, 39}};
expressionvaluewhy
grid.length3number of rows
grid[0].length4length of row 0 (the number of columns
grid[0][0]14row 0, column 0
grid[1][2]78row 1, column 2) not row 2, column 1
grid[2][0]91row 2, column 0
grid[grid.length - 1][grid[0].length - 1]39the bottom-right cell
grid[3][0]throwsrow index 3 in a 3-row array
grid[0][4]throwscolumn index 4 in a 4-column row

The two failures, and this is the useful part, name different lengths:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    at Main.main(Main.java:6)
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
    at Main.main(Main.java:6)

Exam tip: use the length in the message to identify the guilty subscript. for length 3 is the outer array, so the row index is wrong; for length 4 is one row, so the column index is wrong. On a square grid that shortcut disappears, which is why questions that plant a swapped-index bug almost always use a non-square grid.

Try it

Declare a 2-by-5 int grid called tally using new, set the cell in the last row and last column to 7 without writing either number as a literal, and state what tally.length, tally[0].length and tally[0][0] are.

Show answer
int[][] tally = new int[2][5];
tally[tally.length - 1][tally[0].length - 1] = 7;

tally.length is 2 (rows), tally[0].length is 5 (columns), and tally[0][0] is 0, because new fills every cell of an int grid with the default 0. The assignment writes to tally[1][4]. Writing tally[5][2] instead would throw Index 5 out of bounds for length 2: the classic sign of reversed subscripts.

Lesson 4.11 · Unit 4 · CED topic 4.13

Traversing a 2D array

Visiting every cell takes two loops, one inside the other. The outer loop picks a row, the inner loop walks that row's columns, and the inner loop runs completely for each single pass of the outer one. That is row-major order, and it is the default for a reason: it reads the grid the way you read a page.

Swap which index is on the outside and you get column-major order, which visits the same twelve cells in a different sequence. You need it whenever the question is about a column as a unit (the total for each month, the tallest value in each column), because column-major lets you finish one column before starting the next.

Syntax

The two nested loops differ only in which header is outside:

for (int r = 0; r < grid.length; r++) {            // row-major
    for (int c = 0; c < grid[0].length; c++) {
        System.out.print(grid[r][c] + " ");
    }
}

for (int c = 0; c < grid[0].length; c++) {         // column-major
    for (int r = 0; r < grid.length; r++) {
        System.out.print(grid[r][c] + " ");
    }
}

for (int[] row : grid) {                           // nested enhanced for: reading only
    for (int v : row) {
        System.out.print(v + " ");
    }
}

The enhanced version needs int[] as the outer element type, because each element of grid is a row. It always runs in row-major order, it cannot tell you r or c, and assigning to v changes nothing: the same three limits as in one dimension.

Worked example · the visiting order of all twelve cells

Run all three traversals over the same grid:

int[][] grid = {{14, 22, 35, 41},
                {53, 60, 78, 82},
                {91, 17, 26, 39}};
row-major:    14 22 35 41 53 60 78 82 91 17 26 39
column-major: 14 53 91 22 60 17 35 78 26 41 82 39
nested enhanced for: 14 22 35 41 53 60 78 82 91 17 26 39 
visitrow-major (r, c)valuecolumn-major (r, c)value
1–4(0,0) (0,1) (0,2) (0,3)14 22 35 41(0,0) (1,0) (2,0) (0,1)14 53 91 22
5–8(1,0) (1,1) (1,2) (1,3)53 60 78 82(1,1) (2,1) (0,2) (1,2)60 17 35 78
9–12(2,0) (2,1) (2,2) (2,3)91 17 26 39(2,2) (0,3) (1,3) (2,3)26 41 82 39

Both orders end at (2,3) and both visit all twelve cells; only the sequence differs. The nested enhanced loop matches row-major exactly, which is the point: it is shorthand for the first loop, never for the second.

Worked example · one row, one column, one rectangle

Partial traversals drop one of the two loops. To walk a single row, fix r and loop over c; to walk a single column, fix c and loop over r.

int colSum = 0;
for (int r = 0; r < grid.length; r++) {
    colSum += grid[r][2];              // column 2, every row
}
row 1 only:   53 60 78 82
sum of column 2 = 139
sub-rectangle rows 0-1, cols 1-2: 22 35 60 78

Column 2 is 35, 78 and 26, and 35 + 78 + 26 = 139. A sub-rectangle just narrows both loop ranges: for (int r = 0; r <= 1; r++) around for (int c = 1; c <= 2; c++) gives the four cells 22, 35, 60, 78.

Exam tip: a question that says "the elements of each column" is telling you to put c on the outside. Writing row-major and then trying to patch it with a second accumulator is how students run out of time on Question 4.

Try it

Write a loop that adds up only the border cells of a rectangular grid: the cells in the first row, the last row, the first column or the last column. Give the total for the grid above.

Show answer

Visit every cell and keep the ones that sit on an edge:

int borderSum = 0;
for (int r = 0; r < grid.length; r++) {
    for (int c = 0; c < grid[0].length; c++) {
        if (r == 0 || r == grid.length - 1 || c == 0 || c == grid[0].length - 1) {
            borderSum += grid[r][c];
        }
    }
}

Output:

border cells: 14 22 35 41 53 82 91 17 26 39
border sum = 420

Ten of the twelve cells are on the border; only 60 and 78, at (1,1) and (1,2), are interior. A single || chain is enough: do not write four separate loops, because the corner cells would then be counted twice.

Lesson 4.12 · Unit 4 · CED topic 4.14

2D array algorithms

Question 4 on the exam is a 2D array question, and it draws from a short list: total each row, total each column, find the maximum and report where it is, count the cells matching a condition, build a new transformed grid, and compare a cell with its neighbors. Each one is a nested traversal with a different accumulator.

The one genuinely new skill is bounds guarding. A cell on an edge does not have four neighbors, so any code that looks at grid[r - 1][c] has to prove that row exists first.

Rule

Where the accumulator is declared decides what it accumulates. Inside the outer loop, it resets for each column; outside both loops, it totals the whole grid.

public int[] columnTotals() {
    int[] totals = new int[mm[0].length];      // one slot per column
    for (int c = 0; c < mm[0].length; c++) {
        int total = 0;                         // resets for each column
        for (int r = 0; r < mm.length; r++) {
            total += mm[r][c];
        }
        totals[c] = total;
    }
    return totals;
}

Note the return type: a one-dimensional array of column totals, sized by mm[0].length, not mm.length. Getting that one length wrong is the most common way this method fails on a non-square grid.

Worked example · RainfallGrid

Monthly rainfall in millimetres, three stations by four months:

int[][] data = {{12,  0, 34,  8},
                { 5, 21,  3, 40},
                {17,  9, 28,  2}};
rowTotal(0) = 54
rowTotal(1) = 69
rowTotal(2) = 56
totals = 34 30 65 50
countAbove(20) = 4
max = 40 at row 1, column 3

rowTotal(r) fixes the row and loops over columns; countAbove can use the nested enhanced loop because it only reads; and the maximum search carries three variables (the value, its row and its column) all updated together inside one if.

Worked example · tracing columnTotals

The partial total after each cell, printed as the method ran:

crcellpartial totaltotals[c] at the end
00, 1, 212, 5, 1712 → 17 → 3434
10, 1, 20, 21, 90 → 21 → 3030
20, 1, 234, 3, 2834 → 37 → 6565
30, 1, 28, 40, 28 → 48 → 5050

Watch the partial total drop back to a small number at the start of each new column: that is int total = 0; executing again. Move that line above the outer loop and the four answers become 34, 64, 129 and 179: a running total of the whole grid, which is a different question.

Worked example · guarding a neighbour comparison

Count the cells strictly greater than every neighbour directly above, below, left and right. Each test only runs if that neighbour exists.

public boolean isPeak(int r, int c) {
    if (r > 0 && mm[r - 1][c] >= mm[r][c]) {
        return false;                                  // has a row above
    }
    if (r < mm.length - 1 && mm[r + 1][c] >= mm[r][c]) {
        return false;                                  // has a row below
    }
    if (c > 0 && mm[r][c - 1] >= mm[r][c]) {
        return false;                                  // has a column left
    }
    if (c < mm[0].length - 1 && mm[r][c + 1] >= mm[r][c]) {
        return false;                                  // has a column right
    }
    return true;
}

Output:

peak at (0,0) = 12
peak at (0,2) = 34
peak at (1,1) = 21
peak at (1,3) = 40
peak at (2,0) = 17
peak at (2,2) = 28
peaks = 6

The four guards are r > 0, r < mm.length - 1, c > 0 and c < mm[0].length - 1: memorize them as a set. (0,0) counts as a peak because its only two neighbors, 0 and 5, are smaller: a corner cell is judged on the neighbors it actually has.

Exam tip: put the bounds test first and the array access second in the same &&. Short-circuiting then guarantees the subscript never runs when the neighbor is off the grid. Reverse the two halves and the code compiles fine and throws on the first edge cell.

Try it

Write rowTotals(), returning a one-dimensional array of the total for each row, and state what it returns for the rainfall grid. Then say which single length you must change to turn it into columnTotals().

Show answer
public int[] rowTotals() {
    int[] totals = new int[mm.length];         // one slot per ROW
    for (int r = 0; r < mm.length; r++) {
        int total = 0;
        for (int c = 0; c < mm[0].length; c++) {
            total += mm[r][c];
        }
        totals[r] = total;
    }
    return totals;
}

It returns {54, 69, 56}, matching the three rowTotal calls above. The size of the result array is the whole difference: new int[mm.length] for row totals versus new int[mm[0].length] for column totals, with the loop headers swapped to match. On a 3-by-4 grid the two results have different lengths, so a mix-up throws rather than quietly returning nonsense.

Lesson 4.13 · Unit 4 · CED topics 4.15–4.16

Searching and sorting

Linear search walks the collection from the start and stops at the first match, returning its index, or −1 if it reaches the end. It works on anything (arrays, ArrayLists, sorted data or not), and on n elements it may take n comparisons.

Binary search is far faster and demands a price: the data must already be sorted. It looks at the middle element and throws away half the range on every comparison.

Rule

Binary search keeps a live range between low and high:

public static int binary(int[] values, int target) {
    int low = 0;
    int high = values.length - 1;
    while (low <= high) {
        int mid = (low + high) / 2;
        if (values[mid] == target) {
            return mid;
        }
        if (values[mid] < target) {
            low = mid + 1;          // the target is in the right half
        } else {
            high = mid - 1;         // the target is in the left half
        }
    }
    return -1;                      // low passed high: the range is empty
}

Three details decide whether it terminates: low <= high, not <; mid + 1 and mid - 1, never mid; and integer division, which rounds the midpoint down.

Worked example · both searches, counting comparisons

On the sorted array {3, 8, 12, 19, 25, 31, 40, 47, 58}:

  linear used 7 comparisons
linear search for 40 -> index 6
  linear used 9 comparisons
linear search for 22 -> index -1

Binary search for 40, a value that is present:

steplowhighmidvalues[mid]comparison
108425too small → low = 5
258640equal → return 6

And for 22, a value that is absent:

steplowhighmidvalues[mid]comparison
108425too big → high = 3
20318too small → low = 2
323212too small → low = 3
433319too small → low = 4
543, , low > high → return −1

Two comparisons versus seven, and four versus nine. Over an ArrayList the same code swaps values[i] for list.get(i), and for a list of String the test becomes list.get(i).equals(target): == compares references.

Worked example · selection sort against insertion sort

Both sorts run on {29, 10, 14, 37, 13, 25}. Selection sort repeatedly finds the smallest remaining value and swaps it into place:

  start   29 10 14 37 13 25
  pass 1  10 29 14 37 13 25
  pass 2  10 13 14 37 29 25
  pass 3  10 13 14 37 29 25
  pass 4  10 13 14 25 29 37
  pass 5  10 13 14 25 29 37 

Insertion sort takes the next element and slides it back into the sorted front:

  start   29 10 14 37 13 25
  pass 1   10 29 14 37 13 25
  pass 2   10 14 29 37 13 25
  pass 3   10 14 29 37 13 25
  pass 4   10 13 14 29 37 25
  pass 5   10 13 14 25 29 37 

The difference shows in the left-hand columns. After selection sort's pass k, the first k slots hold their final values. After insertion sort's pass k, the first k + 1 slots are sorted among themselves but may all move again: watch 10, 14, 29 become 10, 13, 14, 29 on pass 4.

Exam tip: "which sort produced this intermediate array?" is a standard multiple-choice item. Check whether the front of the array is finished (selection) or merely sorted (insertion), and note that pass 3 changed nothing here: a pass that makes no swap still counts as a pass.

Merge sort is the third one the exam names: split the array in half, sort each half the same way, then merge the sorted halves by repeatedly taking the smaller front element. It is built from recursion: the next lesson.

Try it

Binary-search the same nine-element array for 58 and for 3, and give the sequence of mid values in each case. Then say what binary({40, 12, 58, 3}, 40) returns and why.

Show answer

For 58: mid = 4 (25, too small), mid = 6 (40, too small), mid = 7 (47, too small), mid = 8 (58, equal); returns 8 after four comparisons. For 3: mid = 4 (25, too big), mid = 1 (8, too big), mid = 0 (3, equal); returns 0 after three. The largest value costs more because integer division rounds the midpoint down, so the range creeps toward the top end.

binary({40, 12, 58, 3}, 40) returns −1, even though 40 is sitting at index 0. The array is not sorted, so the first comparison lies: mid is 1, values[1] is 12, and 12 < 40 says "look right", so the half containing the answer is thrown away. The next step compares 58, which is too big, high drops to 1, the range is empty and the method reports −1. Unsorted data makes binary search wrong, not slow.

Lesson 4.14 · Unit 4 · CED topics 4.17–4.18

Recursion

A recursive method calls itself on a smaller version of the same problem. The digits of 4073 are the last digit, 3, plus the digits of 407, and 407 is the same kind of problem, only shorter. Keep shrinking and you reach a case so small it needs no further work: the base case.

Every recursive method needs both halves. A base case that stops, and a recursive call that is guaranteed to move toward it. Miss either and the calls pile up until the JVM gives out with StackOverflowError.

Rule

Base case first, then the call on a smaller argument:

public static int digitSum(int n) {
    if (n < 10) {
        return n;                        // base case: a single digit
    }
    return n % 10 + digitSum(n / 10);    // last digit + the rest, which is smaller
}

n / 10 is strictly smaller than n for every positive n, so the argument marches down to a single digit and stops. Do not try to trace the whole thing in your head: trace one level and trust the method to handle the rest correctly.

Worked example · the call stack for digitSum(4073)

Calls go down the table as each one waits for the next; returned values come back up.

callnn < 10?what it must computevalue returned
digitSum(4073)4073no3 + digitSum(407)3 + 11 = 14
digitSum(407)407no7 + digitSum(40)7 + 4 = 11
digitSum(40)40no0 + digitSum(4)0 + 4 = 4
digitSum(4)4yesbase case4

With print statements added, the real run shows the descent and the unwinding:

  call digitSum(4073)
  call digitSum(407)
  call digitSum(40)
  call digitSum(4)
  digitSum(4) base case returns 4
  digitSum(40) returns 4
  digitSum(407) returns 11
  digitSum(4073) returns 14
digitSum(4073) = 14

Nothing is added until the base case is reached; the additions all happen on the way back. Two more in the same shape: factorial(5) returns 120, and return reverse(s.substring(1)) + s.substring(0, 1); with s.length() <= 1 as the base case turns "stack" into kcats.

Worked example · recursive binary search, and how many calls

The loop becomes a call, and low and high become parameters:

public static int search(int[] values, int target, int low, int high) {
    if (low > high) {
        return -1;                                        // base case: empty range
    }
    int mid = (low + high) / 2;
    if (values[mid] == target) {
        return mid;                                       // base case: found
    }
    if (values[mid] < target) {
        return search(values, target, mid + 1, high);
    }
    return search(values, target, low, mid - 1);
}

Run it on the 16-element array {2, 4, 6, …, 32} looking for 33, which is larger than everything: the worst case.

calllowhighelements in rangemidvalues[mid]
101516716
281581124
3121541328
4141521430
5151511532
616150, returns −1

Five calls examine a midpoint, the range shrinks 16, 8, 4, 2, 1, and a sixth finds the range empty and returns −1. That is why the sorting is worth it: doubling the array to 32 elements adds exactly one comparison, not sixteen.

Exam tip: when a question asks "how many times is the method called", count the empty-range call too if the target is absent, and say which you are counting. Finding the last element here takes 5 calls; failing takes 6. Remove the base case from any recursive method and you get Exception in thread "main" java.lang.StackOverflowError followed by thousands of identical stack frames.

Try it

Write a recursive countDigits(int n) that returns how many digits a positive int has, then draw the call stack for countDigits(4073) and say what goes wrong if the base case is written as if (n == 0).

Show answer
public static int countDigits(int n) {
    if (n < 10) {
        return 1;                        // base case: one digit left
    }
    return 1 + countDigits(n / 10);
}
callncomputesreturns
countDigits(4073)40731 + countDigits(407)4
countDigits(407)4071 + countDigits(40)3
countDigits(40)401 + countDigits(4)2
countDigits(4)4base case1

With if (n == 0) return 0; the method still terminates, 4 / 10 is 0, and still returns 4 for 4073. But countDigits(0) then returns 0 instead of 1, so the answer is wrong for the one input a grader will test. Base the stop on n < 10, which is true for every single-digit value including 0.

Unit 4 review · 10 multiple-choice

Unit 4 review: Data Collections

Ten exam-style questions on the unit that carries the most weight on the exam (the ethics of collecting data, arrays and their traversals, file parsing, wrapper classes, ArrayList, 2D grids, searching and sorting, and recursion) with every stated output taken from a real run of the stimulus.

Multiple choice

  1. A district's attendance app records who was present each day. For every student it stores the name, the student ID, the home address, a photograph, and the GPS position of the phone at the moment the student signs in. The sign-in itself is what marks a student present; the GPS position has never been used for anything else. The team wants to reduce the harm a future data breach could do, without changing what the app is for.

    Which change does the most to reduce that harm?

    Shortening how long data is kept genuinely helps, and a retention limit is worth having. But the app still gathers a year of location history on every student, so a breach next week exposes everything collected since last year.

    Access control protects data from people who should not see it; it does nothing about a stolen database, a misconfigured backup, or a subpoena. Adding a password leaves the same pile of sensitive data sitting there.

    Correct. This is data minimization: collect only what the stated purpose requires. Presence is already established by the sign-in, so the location adds nothing to the purpose while adding a daily movement trail for every student, and data that was never collected cannot leak, be subpoenaed, or be repurposed later.

    Removing names sounds like anonymizing, but the record still carries a home address and a photograph, either of which identifies the student immediately. Swapping an identifier for another identifier is not the same as collecting less.

  2. int[] scores = {4, 7, 2, 9};
    
    for (int s : scores) {
        s = s * 2;
    }
    
    scores[scores.length - 1] = 0;
    
    for (int v : scores) {
        System.out.print(v + " ");
    }

    What is printed as a result of executing the code segment?

    This assumes the first loop doubled the array and the assignment then changed the last element. Only the second half is true: an enhanced for loop hands you a copy of each element, so writing to s never reaches the array.

    This doubles the array and also reads scores[scores.length - 1] as out of bounds or harmless. The array is unchanged by the first loop, and index length - 1 is in bounds: it is scores[scores.length] that would throw.

    This has the enhanced loop right but misses the assignment. scores[scores.length - 1] = 0; is an ordinary indexed write to element 3, and an indexed write is exactly what the enhanced loop cannot do.

    Correct. The first loop is decided on its very first pass: s = s * 2; writes 8 into a copy that is thrown away when the pass ends, and four passes later the array still holds 4, 7, 2, 9. The indexed assignment does change the array, and scores.length - 1 is 3, the last legal index, so the final element becomes 0.

  3. public static int indexOfMax(int[] values) {
        int best = values[0];
        int bestIndex = 0;
        for (int i = 1; i < values.length; i++) {
            if (values[i] >= best) {                 // line 5
                best = values[i];
                bestIndex = i;
            }
        }
        return bestIndex;
    }

    The method is intended to return the smallest index at which the largest value occurs, but on {12, 47, 31, 47, 9} it returns 3. Which of the following replacements for line 5 makes the method work as intended for all arrays of length at least one?

    This spells the champion differently but keeps the >=, so a later equal value still displaces the earlier one. Run on the same array it returns 3 again: the second 47, not the first.

    Reversing the comparison turns the method into a minimum finder. On the same array it returns 4, the index of 9, because the only element that ever beats the running value under this test is a smaller one.

    Correct. Index 3 is where the answer is decided: values[3] is 47 and best is already 47, so >= is true and the index moves, while a strict > is false and the first winner keeps the title. The method then returns 1. Match the operator to the sentence: > keeps the first maximum, >= keeps the last.

    Comparing against values[0] forever compares against the first element instead of the running maximum, so every element larger than 12 overwrites bestIndex and the method returns 3, the last such index.

  4. String[] lines = {"Elm St,18", "", "Oak Ave,22", "Pine Rd,31"};
    int count = 0;
    int total = 0;
    
    for (int i = 0; i < lines.length; i++) {
        int comma = lines[i].indexOf(",");
        if (comma >= 0) {
            total += Integer.parseInt(lines[i].substring(comma + 1));
            count++;
        }
    }
    
    System.out.println(count + " " + total + " " + total / count);

    What is printed as a result of executing the code segment?

    This counts the blank line as a record. On the empty string indexOf(",") returns −1, the guard comma >= 0 is false, and neither total nor count is touched, which is exactly the job that one test is doing.

    The first two numbers are right, but the third is not a double. Both total and count are int, so total / count is integer division, decided entirely by the operand types and not by what is printed.

    Correct. The second pass is where it is settled: the blank line is skipped, so three records contribute 18 + 22 + 31 = 71, and 71 / 3 is integer division, which truncates toward zero to 23 rather than rounding. Casting one operand, (double) total / count, is what would give 23.666666666666668.

    24 is what you get by rounding 23.666… to the nearest whole number. Integer division never rounds; it discards the fractional part. Rounding half up takes the (int) (x + 0.5) idiom, and nothing here does that.

  5. ArrayList<Integer> nums = new ArrayList<Integer>();
    nums.add(12);
    nums.add(34);
    nums.add(56);
    nums.add(78);
    
    nums.remove(1);
    nums.add(0, 90);
    nums.set(2, 11);
    
    System.out.println(nums + " " + nums.size());

    What is printed as a result of executing the code segment?

    Correct. Follow the shifts: remove(1) deletes the element at index 1 and returns 34, leaving [12, 56, 78]; add(0, 90) pushes all three one place right, giving [90, 12, 56, 78]; and the decisive step is set(2, 11), which overwrites whatever is at index 2 after those shifts, 56, not 78.

    This reads nums.remove(1) as removing the value 1. On an ArrayList<Integer> an int argument selects the remove(int index) method, so it removes the element at index 1; you would have to write nums.remove(Integer.valueOf(1)) to remove by value.

    This treats add(0, 90) as an append. The two-argument add inserts at the given index and shifts every later element right, so 90 lands at the front; add(90) with one argument is the one that appends.

    This has the removal and the insertion right but treats set as though it only read the list. set(2, 11) replaces the element at index 2 and returns the old value, so the list really does change, though, unlike add and remove, it never changes size().

  6. // nums holds [9, 4, 3, 12, 2, 6]
    
    for (int i = 0; i < nums.size(); i++) {
        if (nums.get(i) < 5) {
            nums.remove(i);
        }
    }
    
    System.out.println(nums);

    The segment is intended to remove every element less than 5. What is printed as a result of executing it?

    This is what the author intended, and what a backward loop (for (int i = nums.size() - 1; i >= 0; i--)) actually produces on this data. The forward loop does not manage it.

    Correct. The pass at i = 1 decides it: removing the 4 slides the 3 down into index 1, but the loop has already finished with index 1 and moves on to index 2, so the 3 is never examined. The 2 at the end is caught only because it happens to have no removed neighbour before it: adjacent small values are what expose this bug.

    Nothing removed at all would mean the condition never held, but 4, 3 and 2 are all less than 5 and two of them are in fact removed. Note that nums.get(i) is unboxed to an int before the comparison, so < compares numbers, not references.

    That exception belongs to the enhanced for loop, which refuses to let you add to or remove from the collection it is walking. This is an indexed loop, so Java raises no objection: it simply produces the wrong answer, quietly.

  7. // Each Reading has getStation() and getLevel().
    // The readings were built from lines of a data file, so their
    // station names are NOT string literals.
    
    public double averageFor(String station) {
        double total = 0.0;
        int count = 0;
        for (Reading r : readings) {
            if (r.getStation() == station) {            // line 9
                total += r.getLevel();
                count++;
            }
        }
        if (count == 0) {
            return -1.0;
        }
        return total / count;
    }

    The log holds Elm St readings of 18.0, 25.0 and 20.0, one Oak Ave reading and one Elm St North reading, yet averageFor("Elm St") returns −1.0. Which replacement for line 9 makes the method return 21.0 here and work for every station name?

    Writing the same test backwards changes nothing: == on two String references asks whether they are the same object, and a name built at run time by substring is a different object from the literal in the call, so the count stays 0 and the method still returns −1.0.

    compareTo returns 0 for equal strings, so only == 0 is an equality test. Asking for >= 0 accepts every station at or after this one alphabetically, which here counts all five readings and returns 23.2.

    This matches any name that contains the argument, so Elm St North is counted as an Elm St reading and the method returns 23.5. Substring matching is a different question from equality, and the exam plants a name like Elm St North precisely to catch it.

    Correct. equals compares the characters rather than the references, so the three Elm St readings match whatever built their names, and (18.0 + 25.0 + 20.0) / 3 is 21.0. The empty-case guard still works: a station with no readings leaves count at 0 and returns −1.0 instead of dividing by zero.

  8. int[][] grid = {{3, 8, 2, 5},
                    {4, 6, 9, 1},
                    {7, 2, 5, 3}};
    
    for (int c = 0; c < grid[0].length; c++) {
        for (int r = 0; r < grid.length; r++) {
            System.out.print(grid[r][c] + " ");
        }
    }

    What is printed as a result of executing the code segment?

    That is row-major order, which is what you get when r is on the outside. Here c is on the outside, so a whole column is finished before the next column begins.

    Correct. The outer loop fixes a column and the inner loop walks the rows of it, so the first three values printed are column 0 top to bottom (3, 4, 7), and the fourth value, 8, is where the order visibly differs from row-major. All twelve cells are still visited, and the last one is still grid[2][3].

    Six values would mean the outer loop ran only twice. It runs grid[0].length times, which is 4: the number of columns, because grid[0] is one row and its length is how many entries that row has.

    Nothing is out of bounds. The bounds are correctly paired with the subscripts they control: c is tested against grid[0].length and used as the second subscript, and r is tested against grid.length and used as the first.

  9. public static int[] columnTotals(int[][] grid) {
        int[] totals = new int[grid.length];          // line 2
        for (int c = 0; c < grid[0].length; c++) {
            int total = 0;
            for (int r = 0; r < grid.length; r++) {
                total += grid[r][c];
            }
            totals[c] = total;
        }
        return totals;
    }

    Called on the 3-by-4 grid {{3, 8, 2, 5}, {4, 6, 9, 1}, {7, 2, 5, 3}}, the method throws an exception. Which replacement for line 2 makes it return the column totals for every rectangular grid?

    Correct. The subscript that indexes totals is c, and c runs up to grid[0].length - 1, so the array needs one slot per column. grid.length is the number of rows, 3 here, which is why totals[3] throws Index 3 out of bounds for length 3. With the fix the method returns {14, 16, 16, 9}.

    This is big enough never to throw, which is exactly why it hides the error: on the 3-by-4 grid it returns a twelve-element array, {14, 16, 16, 9, 0, 0, 0, 0, 0, 0, 0, 0}, with eight zero totals for columns that do not exist.

    Hard-coding 4 does return {14, 16, 16, 9} for this grid and then throws Index 4 out of bounds for length 4 the first time the method meets a grid with five columns. A length taken from the data is what makes the method general.

    Returning a grid where a one-dimensional array was promised does not even compile: javac reports incompatible types: int cannot be converted to int[] at the assignment and int[][] cannot be converted to int[] at the return.

  10. public static int search(int[] values, int target, int low, int high) {
        if (low > high) {
            return -1;
        }
        int mid = (low + high) / 2;
        if (values[mid] == target) {
            return mid;
        }
        if (values[mid] < target) {
            return search(values, target, mid + 1, high);
        }
        return search(values, target, low, mid - 1);
    }
    int[] a = {5, 9, 12, 18, 23, 27, 34, 41, 50};
    System.out.println(search(a, 7, 0, a.length - 1));

    How many calls to search are made in total, including the original call from println?

    Three is the number of calls that actually examine a midpoint: 23 at index 4, then 9 at index 1, then 5 at index 0. Each one is too big, too big, then too small, so a fourth call is made before the method can answer.

    Correct. The fourth call is the one students forget. After the third call finds 5 < 7 it recurses with low = 1, high = 0; that call examines no element at all, hits the base case low > high, and returns −1. When a target is absent, the empty-range call is what delivers the answer, so it counts.

    Five would be right on a sixteen-element array, where the live range shrinks 16, 8, 4, 2, 1 before emptying. This array holds nine elements, so the range goes 9, 4, 1, 0 and the search is over sooner.

    Nine is the length of the array, and the number of comparisons a linear search would need to report that 7 is absent. Halving the live range on every call is the whole reason binary search is worth keeping the data sorted for.

FRQ 1 · Methods and Control Structures · 22 minutes · 9 points

Write the charge and totalCharge methods of the ParkingGarage class.

Directions

Write your response in Java. Assume that the classes shown below compile exactly as given and that every precondition stated in the question is met: you do not have to check them. Write only the bodies of the two methods you are asked for: do not rewrite the constructor, do not change the given headers, and do not write any code outside the two methods. You may call any method that the class already provides, including one you are writing in another part. Your response is scored on Java that would compile and on whether it produces exactly the values in the examples table, so a method that prints instead of returning, or that has a side effect the question never asked for, loses points even when the arithmetic is right.

A city parking garage charges by the hour, but gives every driver a short free window at the start of a visit so that people who only drop something off are not billed. Once the free window is used up, the garage charges its hourly rate for every started hour past that window: one minute over the free window costs a full hour, sixty-one minutes over costs two hours, and so on. No matter how long a car stays, a single visit is never billed more than the daily maximum of 24.00 dollars.

At the end of each day the attendant hands in a log: the lengths of the day's visits in minutes, written as one string with single commas between the numbers and no spaces, for example 20,75,130. A day with no visits is recorded as the empty string. The class below is used to price a single visit and to price a whole day's log.

public class ParkingGarage {
    /** The dollars charged for each started hour past the free window; always positive. */
    private double hourlyRate;

    /** The number of minutes at the start of a visit that cost nothing; never negative. */
    private int freeMinutes;

    /** Constructs a garage with the given hourly rate and free window. */
    public ParkingGarage(double hourlyRate, int freeMinutes) {
        this.hourlyRate = hourlyRate;
        this.freeMinutes = freeMinutes;
    }

    /** Returns the charge, in dollars, for a single visit lasting minutes minutes,
     *  as described in part (a).
     *  Precondition: minutes >= 0
     */
    public double charge(int minutes) {
        /* to be implemented in part (a) */
    }

    /** Returns the total charge, in dollars, for every visit recorded in log,
     *  as described in part (b).
     *  Precondition: log is not null. log is either the empty string or one or more
     *                non-negative integers separated by single commas, with no spaces
     *                and no comma at either end.
     */
    public double totalCharge(String log) {
        /* to be implemented in part (b) */
    }
}

(a) Write the method charge. A visit of freeMinutes minutes or fewer is free and returns 0.0. Otherwise the garage counts the minutes past the free window and rounds them up to a whole number of started hours, so that any part of an hour is billed as a full hour, and multiplies that count by hourlyRate. If the result is greater than 24.0, the method returns 24.0 instead. Precondition: minutes >= 0. Postcondition: the returned value is at least 0.0 and at most 24.0, and neither instance variable has changed.

public double charge(int minutes)

(b) Write the method totalCharge. The method walks the string log from left to right, using indexOf to find the next comma and substring to cut off the piece in front of it, converts each piece to an int with Integer.parseInt, and adds up the charge for each visit. The empty log returns 0.0. A log with no comma in it holds exactly one visit. Precondition: log is not null and has the form described above. Postcondition: the returned value is the sum of charge applied to every visit length in log, and neither instance variable has changed. In writing totalCharge you may call charge; assume that charge works as specified regardless of what you wrote for part (a).

public double totalCharge(String log)

The table below assumes the garage was built with ParkingGarage g = new ParkingGarage(2.50, 15);. The middle column is what Java actually prints for the returned double, so a charge of two dollars fifty cents appears as 2.5.

CallReturnsWhy
g.charge(15)0.015 is not past the 15-minute free window, so the visit is free.
g.charge(16)2.51 minute past the window starts an hour, so one hour is billed.
g.charge(75)2.560 minutes past the window is exactly one hour, no second hour starts.
g.charge(76)5.061 minutes past the window starts a second hour.
g.charge(135)5.0120 minutes past the window is exactly two hours.
g.charge(2000)24.034 started hours would be 85.0 dollars, so the daily maximum applies.
g.totalCharge("20,75,130")10.02.5 + 2.5 + 5.0: the last piece has no comma after it and must still be counted.
g.totalCharge("130")5.0A log with no comma holds exactly one visit.
g.totalCharge("")0.0An empty log means no visits, so the loop never runs.
Your code
What a reader looks for on this question
  • +1 (a) returns 0.0 when minutes is at most freeMinutes: the test is <=, not <, so a visit of exactly freeMinutes is still free.
  • +1 (a) computes the minutes past the free window as minutes - freeMinutes and converts them to a whole number of started hours, adding one more hour whenever the remainder after dividing by 60 is positive.
  • +1 (a) multiplies the hour count by hourlyRate, caps the result at 24.0 when it is larger, and returns a double.
  • +1 (b) declares and initializes a running total of 0.0 and a local copy of log before the loop, and returns 0.0 for the empty log without entering the loop.
  • +1 (b) finds each separator with indexOf and cuts exactly one visit length per pass with substring, consuming the whole string (including the final piece, which has no comma after it), and terminating.
  • +1 (b) converts each piece with Integer.parseInt, adds the charge for that visit into the accumulator on every pass, and returns the accumulated sum after the loop rather than inside it.
  • +1 uses the given class as specified: both headers match the skeleton exactly, the private fields hourlyRate and freeMinutes are read directly because the code is inside their own class, and the String methods are called with correct arguments (substring(0, comma) and substring(comma + 1)).
  • +1 writes Java that would compile: every local variable is declared with a type, int and double are not confused in the return statements, braces and semicolons are balanced, and no variable is used outside the block in which it is declared.
  • +1 no side effects beyond the specification and no unnecessary code: part (b) calls charge instead of re-deriving the hour arithmetic, neither method assigns to hourlyRate or freeMinutes, neither prints anything, log itself is never treated as something to modify, and the string is walked once.
Show a 9/9 solution

Part (a)

public double charge(int minutes) {
    if (minutes <= freeMinutes) {
        return 0.0;
    }
    int extra = minutes - freeMinutes;
    int hours = extra / 60;
    if (extra % 60 > 0) {
        hours++;
    }
    double amount = hours * hourlyRate;
    if (amount > 24.0) {
        amount = 24.0;
    }
    return amount;
}

Part (b)

public double totalCharge(String log) {
    double total = 0.0;
    String rest = log;
    while (rest.length() > 0) {
        String piece = rest;
        int comma = rest.indexOf(",");
        if (comma >= 0) {
            piece = rest.substring(0, comma);
            rest = rest.substring(comma + 1);
        } else {
            rest = "";
        }
        total = total + charge(Integer.parseInt(piece));
    }
    return total;
}
Where the nine points are earned
  • Correctness 1: the free window. The first if returns 0.0 for every visit of freeMinutes minutes or fewer. The comparison has to be <=. Writing < makes charge(15) return 2.5 instead of 0.0 and costs this point, and it is the single most common slip on this question because the free window sounds like "under fifteen minutes" in English when the specification says "at most fifteen".
  • Correctness 2: started hours. extra is the number of billable minutes, and extra / 60 is integer division, so it throws away the leftover minutes. The test extra % 60 > 0 puts the leftover back as one more full hour. That is what makes charge(16) cost a whole hour while charge(75), whose remainder is exactly zero, does not start a second one.
  • Correctness 3: rate and cap. hours * hourlyRate mixes an int with a double, so Java promotes the product to double, no cast is needed. The second if replaces anything above the daily maximum with 24.0, which is why 2000 minutes returns 24.0 rather than 85.0.
  • Correctness 4: set-up and the empty log. total starts at 0.0 and rest starts as the whole log. When log is "" the while condition is false immediately, the body never runs, and the method returns the initial 0.0. Initializing inside the loop, or returning from inside it, breaks this case.
  • Correctness 5: cutting the pieces. Each pass asks indexOf for the next comma. If there is one, the piece in front of it is taken and rest is reassigned to everything after it; if there is none, the whole of rest is the last visit and rest becomes "" so the loop stops. Forgetting that last branch is how students lose the final visit and report 5.0 for "20,75,130"; forgetting to shrink rest at all is how they write an infinite loop.
  • Correctness 6: accumulate and return. Integer.parseInt(piece) turns the text into an int, charge prices it, and the result is added into total on every pass. The return sits after the loop, so all three visits in "20,75,130" are summed to 10.0. A return inside the loop would report only the first visit.
  • API 1: using the class as given. Both headers are copied from the skeleton unchanged, the private fields are used directly because this code lives inside ParkingGarage, and the String methods are called the way the Java Quick Reference defines them: substring(from, to) stops just before to, and substring(from) runs to the end.
  • API 2: code that compiles. Every local (extra, hours, amount, total, rest, piece, comma) is declared with a type, both methods return a double on every path, and piece is declared before the if so that it is still in scope when it is used afterwards.
  • Design 1, no side effects, no repetition. Part (b) calls charge instead of copying the hour-and-cap arithmetic into a second place, neither method assigns to hourlyRate or freeMinutes, neither prints anything, and the log is traversed exactly once with no extra variables left unused.

Common ways to lose points on this question:

  • Using < instead of <= for the free window, so an exactly fifteen-minute visit is billed.
  • Writing extra / 60 alone and never adding the partial hour, which makes charge(16) return 0.0.
  • Dividing after converting to double (extra / 60.0) and then multiplying, which bills fractions of an hour instead of started hours.
  • Comparing against the cap with >= and returning early, or capping the hour count rather than the dollar amount.
  • Re-deriving the fee inside totalCharge instead of calling charge: this is the design point, and it is lost even if both methods return the right numbers.
  • Printing the total instead of returning it, or changing hourlyRate while computing.

FRQ 2 · Methods and Control Structures · 22 minutes · 9 points

Write the score and rating methods of the PasswordStrength class.

Directions

Write your response in Java. Assume that the class shown below compiles exactly as given and that every stated precondition is met: you do not have to check for a null string. Write only the bodies of the two methods you are asked for: do not rewrite the constructor, do not change the given headers, and do not write any code outside the two methods. You may call a method you are writing in another part of this question. Your response is scored on Java that would compile and on whether it produces exactly the values in the examples table. Remember that this course's Java subset has no charAt: take a single character as the one-character string s.substring(i, i + 1) and compare it with compareTo or equals.

A website rates the passwords people choose and shows them a one-word verdict as they type. The rating is built from a numeric score out of 4. A password earns one point for containing at least one digit, one point for containing at least one uppercase letter, one point for containing at least one lowercase letter, and one point for being long enough. Each of those four things is worth at most one point no matter how many characters satisfy it: a password of twelve digits still earns exactly one point for digits.

"Long enough" is decided by the site, not by the class, so the minimum length is stored in the object when it is constructed. The score is then turned into the word the user sees. The class below does both jobs.

public class PasswordStrength {
    /** The smallest length that earns the length point; always positive. */
    private int minLength;

    /** Constructs a password checker with the given minimum length. */
    public PasswordStrength(int minLength) {
        this.minLength = minLength;
    }

    /** Returns the strength score of pw, a value from 0 through 4,
     *  as described in part (a).
     *  Precondition: pw is not null.
     */
    public int score(String pw) {
        /* to be implemented in part (a) */
    }

    /** Returns the one-word rating of pw, as described in part (b).
     *  Precondition: pw is not null.
     */
    public String rating(String pw) {
        /* to be implemented in part (b) */
    }
}

(a) Write the method score. Walk pw one character at a time, taking each character as the one-character string pw.substring(i, i + 1). A one-character string ch is a digit when ch.compareTo("0") >= 0 and ch.compareTo("9") <= 0; uppercase letters and lowercase letters are tested the same way, against "A" and "Z" and against "a" and "z". Award one point if at least one digit appears anywhere in pw, one point if at least one uppercase letter appears, one point if at least one lowercase letter appears, and one point if the length of pw is at least minLength. Return the total. Precondition: pw is not null; pw may be the empty string. Postcondition: the returned value is an int from 0 through 4 inclusive, and minLength has not changed.

public int score(String pw)

(b) Write the method rating. It returns "weak" when the score is 0 or 1, "fair" when the score is 2 or 3, and "strong" when the score is 4. Precondition: pw is not null. Postcondition: the returned value is exactly one of the three words above, in lowercase, and minLength has not changed. In writing rating you may call score; assume that score works as specified regardless of what you wrote for part (a).

public String rating(String pw)

The table below assumes the checker was built with PasswordStrength p = new PasswordStrength(8);.

Callscore returnsrating returnsWhy
"abc"1weakLowercase only, and 3 is shorter than 8.
"abcdefgh"2fairLowercase plus the length point; no digit and no uppercase.
"Abcdefgh"3fairUppercase, lowercase and length; still no digit.
"Abcdef12"4strongAll three categories appear and the length is 8.
"AB12"2fairDigit and uppercase only: two digits and two capitals are still one point each.
""0weakThe loop never runs and 0 is shorter than 8.
Your code
What a reader looks for on this question
  • +1 (a) declares and initializes one boolean flag (or equivalent) for each of the three character categories before the loop starts, so that a category found early is remembered to the end.
  • +1 (a) loops over every index of pw from 0 through pw.length() - 1, missing neither the first nor the last character and running off neither end, and extracts one character with pw.substring(i, i + 1).
  • +1 (a) tests the three categories correctly on that one-character string, using compareTo range comparisons against "0"/"9", "A"/"Z" and "a"/"z" rather than == on strings.
  • +1 (a) counts each category at most once, however many characters match it, so "AB12" scores 2 and not 4.
  • +1 (a) adds the fourth point exactly when pw.length() >= minLength, comparing against the instance variable and not a hard-coded number, and handles the empty string by scoring 0 rather than failing.
  • +1 (a) returns the accumulated total, an int from 0 through 4, after the loop rather than inside it.
  • +1 uses the given class as specified: both headers match the skeleton exactly, minLength is read directly because the code is inside its own class, and String methods are called with correct arguments: substring(i, i + 1), length() with parentheses, compareTo used for its sign rather than compared to a letter.
  • +1 writes Java that would compile: every local variable is declared with a type, rating returns a String on every possible path, score returns an int, braces and semicolons are balanced, and no variable is used outside the block where it is declared.
  • +1 no side effects beyond the specification and no unnecessary code: rating calls score once instead of repeating the character scan, neither method assigns to minLength, neither prints when it is asked to return, pw is never reassigned, and the password is walked only once.
Show a 9/9 solution

Part (a)

public int score(String pw) {
    boolean hasDigit = false;
    boolean hasUpper = false;
    boolean hasLower = false;
    for (int i = 0; i < pw.length(); i++) {
        String ch = pw.substring(i, i + 1);
        if (ch.compareTo("0") >= 0 && ch.compareTo("9") <= 0) {
            hasDigit = true;
        }
        if (ch.compareTo("A") >= 0 && ch.compareTo("Z") <= 0) {
            hasUpper = true;
        }
        if (ch.compareTo("a") >= 0 && ch.compareTo("z") <= 0) {
            hasLower = true;
        }
    }
    int points = 0;
    if (hasDigit) {
        points++;
    }
    if (hasUpper) {
        points++;
    }
    if (hasLower) {
        points++;
    }
    if (pw.length() >= minLength) {
        points++;
    }
    return points;
}

Part (b)

public String rating(String pw) {
    int s = score(pw);
    if (s <= 1) {
        return "weak";
    }
    if (s <= 3) {
        return "fair";
    }
    return "strong";
}
Where the nine points are earned
  • Correctness 1: the flags. Three boolean variables are declared and set to false before the loop. That placement is the whole idea of the method: the loop can only ever flip a flag from false to true, so whatever it finds at index 0 is still remembered at the last index. Declaring the flags inside the loop resets them on every pass and leaves the method reporting only what the final character happened to be.
  • Correctness 2: the traversal. The loop runs i from 0 while i < pw.length(), which visits every index once and stops before the string ends. pw.substring(i, i + 1) is the AP-subset way to pull out a single character: the second argument is the index just past the piece you want, so this returns exactly one character. Using <= in the loop condition throws StringIndexOutOfBoundsException.
  • Correctness 3: the category tests. compareTo returns a negative number, zero, or a positive number according to alphabetical order, so ch.compareTo("0") >= 0 && ch.compareTo("9") <= 0 says "this character sits between 0 and 9 inclusive". The same pattern tests the two letter ranges. The three tests are separate if statements, not else if, which is harmless here because no character can be in two ranges at once, but writing them as independent tests is the habit that keeps this kind of method correct.
  • Correctness 4, at most one point per category. Because the loop only sets flags and the points are counted afterward, a password of four digits sets hasDigit four times and still scores one point for digits. This is exactly the case the examples table checks with "AB12", which has two capitals and two digits and scores 2. A counter that increments inside the loop would report 4 there and lose this point.
  • Correctness 5: the length point and the empty string. The last if compares pw.length() with the instance variable minLength, not with a literal 8, so the same class works for a site that wants twelve. For "" the loop body never executes, all three flags stay false, the length test fails, and the method returns 0, no special case is needed.
  • Correctness 6: the return. points is accumulated after the loop and returned once, so the value is an int from 0 through 4. Part (b) then maps that number onto the three words with two tests and a fall-through return, which covers 0 through 4 with no gap and no overlap and guarantees a String on every path.
  • API 1: using the class as given. Both headers are copied from the skeleton unchanged. minLength is read directly because this code lives inside PasswordStrength; from outside the class it would have to go through an accessor. length() is a method on a String and needs its parentheses: the parenthesis-free length belongs to arrays.
  • API 2: code that compiles. hasDigit, hasUpper, hasLower, ch, points and s are all declared with a type; ch is declared inside the loop where it is used and nowhere else; score returns an int and rating a String; and the string comparisons use compareTo rather than <, which does not compile on objects.
  • Design 1, no side effects, no repetition. rating calls score once and stores the result instead of calling it three times or copying the scan; neither method assigns to minLength or to pw; neither prints when it was asked to return; and no variable is declared that is never used.

Common ways to lose points on this question:

  • Incrementing a counter inside the loop instead of setting a flag, so a password with several digits scores more than one point for digits.
  • Declaring the flags inside the loop, which resets them every pass and leaves the score describing only the last character.
  • Using == or < on the one-character strings; == compares references and < does not compile on a String.
  • Writing i <= pw.length(), or substring(i, i + 2), and running off the end of the string.
  • Comparing the length against a literal 8 instead of minLength, which passes the examples table but fails any other checker object.
  • Rewriting the whole character scan inside rating rather than calling score: this is the design point, and it is lost even when both methods return the right words.
  • Returning "Weak" or "STRONG"; the specification names the words in lowercase and a grader compares them with equals.

FRQ 3 · Class Design · 22 minutes · 9 points

A coffee shop punch card

This is a Question 2 task: you are handed a description of an object and you write the whole class (the instance variables, the constructor, and every method) from nothing. Nobody gives you a skeleton to fill in, so the first thing a reader checks is whether the class you wrote would compile on its own.

A campus coffee shop gives every regular a paper punch card. The card belongs to one named owner. Each drink the owner buys adds one punch to the card. When the fifth punch is added the shop tears the card down: those five punches become one free drink, and the punch count goes back to zero, so the sixth purchase is punch number one on a fresh card. Free drinks pile up on the card until the owner spends them, and a free drink can only be spent if there is one there to spend.

Write the complete class CoffeeCard to model one punch card.

Instance variables. Your class needs exactly three pieces of state, and all three must be private: the owner's name as a String, the number of punches currently on the card as an int, and the number of unspent free drinks as an int. Choose the names yourself: a reader scores the encapsulation, not your spelling.

Constructor and methods. Your class must declare exactly these members, with these headers. The behavior of each one is described below.

public CoffeeCard(String owner)

public void buy()

public int getFreeDrinks()

public boolean redeem()

public String toString()

CoffeeCard(String owner): creates a brand-new card for the given owner, with zero punches and zero free drinks. Precondition: owner is not null.

buy(): records one purchase. It adds one punch to the card. If that punch is the fifth one, it instead converts the five punches into one more free drink and sets the punch count back to 0. It returns nothing.

getFreeDrinks(): returns the number of unspent free drinks on the card. It must not change the card.

redeem(): spends one free drink. If the card has at least one unspent free drink, redeem removes one from the card and returns true. If the card has no free drinks, it changes nothing at all and returns false. The punch count is never touched by redeem.

toString(): returns the owner's name, a space, the punch count, a slash, the digit 5, a comma, a space, the free-drink count, a space, and the word free. For a card belonging to Maya with one punch and one free drink that string is exactly Maya 1/5, 1 free.

The table traces one card from the moment it is created through six calls to buy and two calls to redeem. Every value in it comes from running the class. The punch count is the number before the slash.

CallReturnsgetFreeDrinks()toString()
CoffeeCard card = new CoffeeCard("Maya");: 0Maya 0/5, 0 free
card.buy(); (1st)no value (void)0Maya 1/5, 0 free
card.buy(); (2nd)no value (void)0Maya 2/5, 0 free
card.buy(); (3rd)no value (void)0Maya 3/5, 0 free
card.buy(); (4th)no value (void)0Maya 4/5, 0 free
card.buy(); (5th)no value (void)1Maya 0/5, 1 free
card.buy(); (6th)no value (void)1Maya 1/5, 1 free
card.redeem();true0Maya 1/5, 0 free
card.redeem(); (again)false0Maya 1/5, 0 free
Directions

Write the complete CoffeeCard class, including the class header, the private instance variables, the constructor, and all four methods, so that it behaves exactly as described above and produces every value in the examples table. Your response is scored on Java that would compile: declare a type for every variable, match the given headers exactly, and balance your braces. Write the class so that it stands alone, no main method, no import, no printing inside any method, and nothing outside the class. You have 22 minutes; a Question 2 response is worth 9 points, and correctness is worth more than elegance, so write the straightforward version first and only tidy it if time is left.

Your code
Scoring notes, 9 points
  • +1 declares public class CoffeeCard and everything the question asks for lives inside that one class body, no main method, no second class, no code outside the braces.
  • +1 declares all three instance variables and declares them private: a String for the owner and two int counters for punches and free drinks.
  • +1 constructor public CoffeeCard(String owner) stores the parameter in the owner field and sets both counters to 0 (writing them explicitly or letting them default is both fine, but the field must be assigned the parameter, not itself).
  • +1 public void buy() adds exactly one punch on every call, and on the fifth punch adds one to the free-drink count and resets the punch count to 0: the test is == 5 (or >= 5), never > 5, and the reset happens in the same call.
  • +1 public int getFreeDrinks() returns the free-drink counter itself and modifies nothing.
  • +1 public boolean redeem() guards on the free-drink count being greater than 0, decreases it by one and returns true inside the guard, and returns false without changing any field otherwise: both paths return.
  • +1 public String toString() builds and returns owner, space, punch count, /5, comma, space, free count, space, free, in that order with those exact separators.
  • +1 every header matches the required return type and parameter list exactly (void for buy, int for getFreeDrinks, boolean for redeem, String for toString), and every non-void method returns a value of that type on every path.
  • +1 design and encapsulation: the fields stay private and are never exposed, no accessor changes state, no method prints instead of returning, redeem leaves the punch count alone, and no variable is declared that is never used.
Show a 9/9 solution

Here is a response that earns all nine points. It was compiled with javac and run against the call sequence in the examples table.

public class CoffeeCard {
    private String owner;
    private int punches;
    private int freeDrinks;

    public CoffeeCard(String owner) {
        this.owner = owner;
        punches = 0;
        freeDrinks = 0;
    }

    public void buy() {
        punches++;
        if (punches == 5) {
            freeDrinks++;
            punches = 0;
        }
    }

    public int getFreeDrinks() {
        return freeDrinks;
    }

    public boolean redeem() {
        if (freeDrinks > 0) {
            freeDrinks--;
            return true;
        }
        return false;
    }

    public String toString() {
        return owner + " " + punches + "/5, " + freeDrinks + " free";
    }
}

Printing the card after it is created, after each of six calls to buy, and around two calls to redeem gives exactly this:

Maya 0/5, 0 free
Maya 1/5, 0 free
Maya 2/5, 0 free
Maya 3/5, 0 free
Maya 4/5, 0 free
Maya 0/5, 1 free
Maya 1/5, 1 free
true
Maya 1/5, 0 free
false
Maya 1/5, 0 free
Where the nine points are earned
  • Class header (use of the API and syntax, 1 of 2): public class CoffeeCard { opens the file and the closing brace ends it. There is no main, no import and no second class, so the file compiles on its own, which is the only way the other eight points can be awarded at all.
  • Private instance variables (correctness, 1 of 6): three fields, one per piece of state the description names, each marked private. The counters are int because they count whole punches and whole drinks; the owner is a String.
  • Constructor (correctness, 2 of 6): the header matches public CoffeeCard(String owner). Because the parameter and the field share the name owner, the assignment must be this.owner = owner;: owner = owner; compiles and silently does nothing. Both counters are set to 0, so a new card starts empty.
  • buy (correctness, 3 of 6): one punch is added on every call, then the fifth punch is converted. Testing punches == 5 after the increment is what makes the card reset on the fifth call rather than the sixth, and setting punches = 0 inside the same if is what makes the sixth call read 1/5.
  • getFreeDrinks (correctness, 4 of 6): a one-line accessor that returns the counter and touches nothing. An accessor that also changed state would cost this point and the design point together.
  • redeem (correctness, 5 of 6): the guard freeDrinks > 0 separates the two cases the specification names. Inside the guard the count drops by one and the method returns true; outside it the method returns false having changed nothing, which is why the second redeem in the table prints false and leaves the card reading 1/5, 0 free.
  • toString (correctness, 6 of 6): one concatenation, in the exact order the specification gives, with the literal "/5, " and " free" carrying the punctuation. The int fields convert to text automatically when they are added to a String.
  • Return types and headers (use of the API and syntax, 2 of 2): buy is void and returns nothing, getFreeDrinks returns an int, redeem returns a boolean on both paths, and toString returns a String. Every header is spelled exactly as the question gave it, so a client written against the specification links against this class.
  • Program design and encapsulation (design, 1 of 1): nothing is public except the five members the question asked for, no method prints, redeem does not disturb the punch count, and there are no leftover local variables or a second pass over anything.

Common ways to lose points on this question:

  • Testing punches > 5 instead of punches == 5. The card then rewards the owner on the sixth punch, and every row of the examples table from the fifth buy onward is wrong.
  • Incrementing after the test, if (punches == 4) { ... } punches++;, which is a real off-by-one, not a style choice. Do the increment first, then ask whether the card is full.
  • Writing owner = owner; in the constructor. It compiles, the field stays null, and toString returns null 0/5, 0 free.
  • Making redeem return void, or printing "no free drinks" instead of returning false. The header is public boolean redeem(); a method asked to return must never print.
  • Declaring the fields public, or adding a setter nobody asked for. Either one costs the design point, and public fields also undercut the encapsulation the whole question is testing.
  • Adding a main method to test the class and leaving it in. The response is supposed to be a class, not a program, and stray code outside the class body will not compile.

FRQ 4 · Class Design · 22 minutes · 9 points

A student ticket that discounts a price

This is the other shape Question 2 takes: a working class is given to you, and you write a subclass that reuses it. The whole question turns on one fact: a subclass inherits the superclass's methods but cannot see its private fields. Everything the subclass needs to know about the price it has to ask for through a method.

A box office sells tickets to campus events. Every ticket knows its event and its base price. Students get a percentage off that base price, but the box office caps the student discount at 50 percent, and a negative discount makes no sense, so a percentage below 0 is treated as 0 and a percentage above 50 is treated as 50. The discounted price is charged to the nearest cent.

The Ticket class below is given to you and is already written. You may not change it.

public class Ticket {
    private String event;
    private double basePrice;

    public Ticket(String event, double basePrice) {
        this.event = event;
        this.basePrice = basePrice;
    }

    public double getPrice() {
        return basePrice;
    }

    public String getEvent() {
        return event;
    }

    public String toString() {
        return event + " ticket, $" + getPrice();
    }
}

Write the complete class StudentTicket, a subclass of Ticket.

Instance variable. StudentTicket adds exactly one piece of state of its own, and it must be private: an int holding the discount percentage after it has been capped. It must not redeclare the event or the base price: those already exist in Ticket.

Constructor and methods. Your class must declare exactly these members, with these headers.

public StudentTicket(String event, double basePrice, int percentOff)

public double getPrice()

public int getPercentOff()

public String toString()

StudentTicket(String event, double basePrice, int percentOff): passes event and basePrice to the superclass constructor with a call to super, which must be the first statement in the constructor body. It then stores percentOff after capping it into the range 0 through 50: a value below 0 is stored as 0, a value above 50 is stored as 50, and a value already in range is stored unchanged. Precondition: basePrice is at least 0.

getPrice(): overrides the inherited method. It returns the superclass price reduced by the stored percentage, rounded to the nearest cent. Round with the (int)(x * 100 + 0.5) / 100.0 idiom; Math.round is outside the AP Java subset. The base price is private in Ticket, so the only way to read it is super.getPrice().

getPercentOff(): returns the capped percentage that was stored, not the raw argument the caller passed in.

toString(): overrides the inherited method. It returns whatever super.toString() returns, followed by a space, an opening parenthesis, the stored percentage, and the text % student discount).

Every value in the table below was produced by running the two classes. Note the third and fourth rows: because Ticket.toString calls getPrice(), and your override replaces it, super.toString() already reports the discounted price.

CallgetPercentOff()getPrice()toString()
new Ticket("Recital", 40.00): 40.0Recital ticket, $40.0
new StudentTicket("Recital", 40.00, 25)2530.0Recital ticket, $30.0 (25% student discount)
new StudentTicket("Recital", 40.00, 60)5020.0Recital ticket, $20.0 (50% student discount)
new StudentTicket("Recital", 40.00, -5)040.0Recital ticket, $40.0 (0% student discount)
new StudentTicket("Jazz Night", 19.99, 15)1516.99Jazz Night ticket, $16.99 (15% student discount)
Directions

Write the complete StudentTicket class, including the class header with extends, the private instance variable, the constructor, and all three methods, so that it behaves exactly as described and produces every value in the examples table. You must use the given Ticket class as specified: do not rewrite it, do not copy its fields into your subclass, and reach its data only through the methods it makes public. Your response is scored on Java that would compile, so match the given headers exactly and put super(...) first in the constructor. Write the class so that it stands alone, no main method and no printing inside any method. You have 22 minutes for 9 points.

Your code
Scoring notes, 9 points
  • +1 declares one private int instance variable for the capped percentage and declares no other field, the event and the base price are inherited, not redeclared.
  • +1 the constructor's first statement is super(event, basePrice);, passing the two parameters through in that order.
  • +1 the constructor caps the percentage into 0 through 50 before storing it, so a value below 0 becomes 0 and a value above 50 becomes 50, and stores the capped value in the field.
  • +1 public double getPrice() reads the undiscounted price with super.getPrice() and applies the stored percentage, for example super.getPrice() * (100 - percentOff) / 100.0.
  • +1 getPrice rounds the result to the nearest cent with (int)(x * 100 + 0.5) / 100.0 so that 19.99 at 15 percent returns 16.99 rather than 16.9915.
  • +1 public String toString() returns super.toString() followed by a space, (, the stored percentage, and % student discount): it calls the superclass version rather than rebuilding the event and price by hand.
  • +1 the class header is public class StudentTicket extends Ticket, the file holds that one class, and the two overriding methods have exactly the inherited signatures so they override rather than overload.
  • +1 every header matches the required return type and parameter list, including public int getPercentOff() returning the stored int, and every non-void method returns a value of its declared type on every path.
  • +1 design and encapsulation: no attempt to read Ticket's private basePrice or event directly, no second copy of inherited data, no accessor that changes state, no printing where a value is returned, and no unused variables.
Show a 9/9 solution

Here is a response that earns all nine points. It was compiled with javac alongside the given Ticket class and run against every row of the examples table.

public class StudentTicket extends Ticket {
    private int percentOff;

    public StudentTicket(String event, double basePrice, int percentOff) {
        super(event, basePrice);
        if (percentOff < 0) {
            percentOff = 0;
        }
        if (percentOff > 50) {
            percentOff = 50;
        }
        this.percentOff = percentOff;
    }

    public double getPrice() {
        double reduced = super.getPrice() * (100 - percentOff) / 100.0;
        return (int) (reduced * 100 + 0.5) / 100.0;
    }

    public int getPercentOff() {
        return percentOff;
    }

    public String toString() {
        return super.toString() + " (" + percentOff + "% student discount)";
    }
}

Creating the four student tickets from the table and printing getPercentOff(), getPrice() and the ticket itself prints exactly this:

30.0
Recital ticket, $30.0 (25% student discount)
50
20.0
Recital ticket, $20.0 (50% student discount)
0
40.0
Recital ticket, $40.0 (0% student discount)
16.99
Jazz Night ticket, $16.99 (15% student discount)
Where the nine points are earned
  • The new instance variable (correctness, 1 of 6): one private int percentOff; and nothing else. A subclass inherits the superclass's state even though it cannot see it, so declaring a second event or basePrice would create a shadow copy that super.getPrice() knows nothing about.
  • super first (correctness, 2 of 6): super(event, basePrice); opens the constructor body. The superclass part of the object has to be built before the subclass part, and Java enforces that: a super call anywhere but the first line is a compile error, and the whole part scores zero.
  • Capping the percentage (correctness, 3 of 6): two separate if statements pull the argument into range before it is stored, which is what makes 60 come back as 50 and −5 come back as 0. Capping in the constructor rather than in getPrice means the stored value is already clean, so getPercentOff and toString get the capped number for free.
  • Reaching the base price (correctness, 4 of 6): super.getPrice() is the only legal way in. Multiplying by (100 - percentOff) / 100.0 takes the percentage off; the 100.0 forces double arithmetic, so 25 percent off 40.00 is 30.0 and not the 0.0 that integer division would produce.
  • Rounding to the nearest cent (correctness, 5 of 6): (int) (reduced * 100 + 0.5) / 100.0 scales to cents, adds a half cent, truncates with the cast, and scales back. On the Jazz Night ticket the exact value is 16.9915, which becomes 1699 cents and then 16.99.
  • toString (correctness, 6 of 6): the override delegates to super.toString() and appends the discount text. Because Ticket.toString calls getPrice() and that call is dispatched to the override, the inherited half of the string already shows the student price, which is why the model output reads $30.0 and not $40.0.
  • Class header and overriding (use of the API and syntax, 1 of 2): public class StudentTicket extends Ticket. Both overrides repeat the inherited signature exactly, so they replace the inherited behavior; changing a parameter list would silently overload instead, and the inherited version would keep running.
  • Headers and return types (use of the API and syntax, 2 of 2): getPrice returns double, getPercentOff returns int, toString returns String, and the constructor takes the three parameters in the given order. Every local variable is declared with a type, and the class would compile untouched next to Ticket.
  • Program design and encapsulation (design, 1 of 1): the subclass never reaches into Ticket's private fields, keeps its own field private, adds no method nobody asked for, prints nothing, and recomputes nothing it could ask the superclass for.

Common ways to lose points on this question:

  • Using basePrice directly in the subclass. It is private in Ticket, so javac answers error: basePrice has private access in Ticket and the part earns nothing. Ask super.getPrice() instead.
  • Putting this.percentOff = percentOff; above the super call. That is error: call to super must be first statement in constructor: a compile error, not a style warning.
  • Capping with a single chained test, or capping inside getPrice while getPercentOff still returns the raw argument, so a discount of 60 reports 60 but charges half price.
  • Writing super.getPrice() * (100 - percentOff) / 100 with an integer 100. The whole expression becomes integer division on a double operand only if you are lucky with the order; write 100.0 and stop worrying.
  • Reaching for Math.round, which is outside the AP Java subset, or skipping the rounding entirely and returning 16.9915.
  • Rebuilding the string by hand as getEvent() + " ticket, $" + getPrice() + .... It happens to print the same characters here, but the question said to call super.toString(), and duplicating the superclass's formatting is exactly the kind of unnecessary code the design point is looking for.

FRQ 5 · Data Analysis with ArrayList · 22 minutes · 9 points

Averaging and pruning an air-quality log

Directions

Write your answer to each part in the space below. Your response is scored on Java that would compile: every method header must match the header given in the question exactly, every variable must be declared, and every object you create must use a constructor that exists. Use the given classes as specified: read a Reading through its accessor methods rather than reaching into its private fields, and use the ArrayList methods size, get and remove rather than writing your own. Do not write anything the question does not ask for: a method that is told to return a value must return it, not print it, and it must not change data the question does not tell it to change. You have about 22 minutes.

A regional agency records fine-particulate readings from several monitoring stations. Each reading knows which station recorded it, which day of the study it belongs to, and the PM2.5 concentration measured, in micrograms per cubic meter. Readings arrive in the order they are taken, and the agency keeps them in that order.

The Reading class below is given to you. You will not write any part of it; you will only call its accessor methods.

public class Reading
{
    /** The name of the station that recorded this reading; never null. */
    private String station;

    /** The day of the study on which this reading was taken; 1 is the first day. */
    private int day;

    /** The PM2.5 concentration of this reading, in micrograms per cubic meter. */
    private double pm25;

    public Reading(String s, int d, double p)
    {
        station = s;
        day = d;
        pm25 = p;
    }

    /** @return the name of the station that recorded this reading */
    public String getStation()
    {
        return station;
    }

    /** @return the day of the study on which this reading was taken */
    public int getDay()
    {
        return day;
    }

    /** @return the PM2.5 concentration of this reading */
    public double getPm25()
    {
        return pm25;
    }
}

The class AirQualityLog stores the readings collected so far. You will write two of its methods. The declarations of the two methods, with their preconditions and postconditions, are shown below.

public class AirQualityLog
{
    /** The readings in this log, in the order they were recorded.
     *  Guaranteed not to be null, but it may be empty.
     */
    private ArrayList<Reading> readings;

    /** Returns the mean of the pm25 values of all readings in this log that were
     *  recorded at station, or -1.0 if this log contains no reading from station.
     *  Precondition: station is not null.
     *  Postcondition: readings is unchanged.
     */
    public double averageFor(String station)
    {  /* to be implemented in part (a) */  }

    /** Removes from this log every reading whose pm25 value is strictly less than
     *  limit, and returns the number of readings removed.
     *  Postcondition: the readings that were not removed appear in readings in the
     *                 same relative order in which they appeared before the call.
     */
    public int removeBelow(double limit)
    {  /* to be implemented in part (b) */  }

    // There may be instance variables, constructors, and methods that are not shown.
}

Part (a). Write the method averageFor. The method returns the mean of the pm25 values of every reading in readings whose station name is equal to station. If no reading in the log came from that station, and in particular if readings is empty, the method returns -1.0. The log itself must not change. Write the complete method, beginning with the header public double averageFor(String station).

Part (b). Write the method removeBelow. The method removes from readings every reading whose pm25 value is strictly less than limit and returns how many readings it removed. Readings whose value is exactly limit stay. Every reading that is not removed must still be in readings afterwards, in its original relative order. Write the complete method, beginning with the header public int removeBelow(double limit).

In the examples below, log is an AirQualityLog whose readings list holds these six readings, in this order: North/day 1/12.0, South/day 1/22.0, North/day 2/25.0, East/day 2/9.0, North/day 3/18.5, South/day 3/14.0. Each row starts from that original list.

CallValue returnedContents of readings afterwards
log.averageFor("North")18.5unchanged (all six readings)
log.averageFor("South")18.0unchanged (all six readings)
log.averageFor("East")9.0unchanged (all six readings)
log.averageFor("West")-1.0unchanged (all six readings)
log.removeBelow(15.0)3South/1/22.0, North/2/25.0, North/3/18.5
log.removeBelow(5.0)0unchanged (all six readings)
log.removeBelow(30.0)6empty
Your code
Scoring notes, where the 9 points are
  • +1 Traversal. Loops over the elements of readings in both parts with bounds that miss no first or last element and run off no end: an index from 0 while i < readings.size() in part (a), and in part (b) a loop that visits every index of the list even while it is shrinking.
  • +1 Accessing elements with get. Obtains each element as readings.get(i) and reads its data through the given accessors getStation() and getPm25(), never by naming a field of Reading.
  • +1 Comparison logic. Compares station names with equals and not with == in part (a), and in part (b) tests getPm25() < limit strictly, so a reading exactly equal to limit survives.
  • +1 Accumulation. Declares and initializes a running sum and a match counter before the loop in part (a), and adds getPm25() to the sum and increments the counter on exactly the matching readings, not on every reading.
  • +1 Correct removal without skipping. In part (b), removes with readings.remove(i) in a way that still examines every reading: a loop running backward from readings.size() - 1 down to 0, or a forward loop that does not increment the index after a successful removal, and increments a removal counter on each removal.
  • +1 Return. Part (a) returns -1.0 when the counter is 0 and otherwise returns sum / count after the loop finishes, with the division done in double; part (b) returns the removal count after the loop, not from inside it.
  • +1 Uses the given classes as specified. Uses the ArrayList API correctly (size(), get(int), remove(int) on an ArrayList of Reading), and calls the supplied Reading methods instead of re-implementing or bypassing them.
  • +1 Java that would compile. Both headers match public double averageFor(String station) and public int removeBelow(double limit) exactly, every local variable is declared with a type, the element pulled out of the list is typed Reading, and braces, parentheses and semicolons balance.
  • +1 Design and encapsulation. No side effects beyond the specification and no unnecessary code: averageFor leaves readings untouched, removeBelow changes it only by removing and preserves the survivors' relative order, neither method prints when it is asked to return, and neither traverses the list a second time to do work one pass already did.
Show a 9/9 solution

Part (a).

public double averageFor(String station)
{
    double sum = 0.0;
    int count = 0;
    for (int i = 0; i < readings.size(); i++)
    {
        Reading r = readings.get(i);
        if (r.getStation().equals(station))
        {
            sum += r.getPm25();
            count++;
        }
    }
    if (count == 0)
    {
        return -1.0;
    }
    return sum / count;
}

Part (b).

public int removeBelow(double limit)
{
    int removed = 0;
    for (int i = readings.size() - 1; i >= 0; i--)
    {
        if (readings.get(i).getPm25() < limit)
        {
            readings.remove(i);
            removed++;
        }
    }
    return removed;
}

Part (a) is the standard conditional-average shape: two accumulators declared before the loop, one traversal, a guard for the empty case, and a division that happens once, at the end. Because sum is a double, sum / count is double division even though count is an int, so no cast is needed. The count == 0 test has to come before the division, not after it: dividing by zero in double arithmetic does not throw, it quietly produces NaN, and a method that returns NaN where the specification says -1.0 loses the point silently.

Part (b) walks the list backward. That is the whole trick, and it works for one reason: removing the element at index i shifts only the elements after i, and when you are moving toward 0 you have already finished with all of them. A forward loop shifts the unexamined elements down into positions you have already passed. A driver that builds the six-reading example, calls each method on a fresh copy of it and prints each surviving reading as station/day/value produces exactly the table values:

averageFor("North") -> 18.5
averageFor("South") -> 18.0
averageFor("East")  -> 9.0
averageFor("West")  -> -1.0
removeBelow(15.0) -> 3
readings after    -> [South/1/22.0, North/2/25.0, North/3/18.5]
removeBelow(30.0) -> 6
readings after    -> []
removeBelow(5.0)  -> 0
readings after    -> [North/1/12.0, South/1/22.0, North/2/25.0, East/2/9.0, North/3/18.5, South/3/14.0]
Where each of the 9 points is earned
  • Traversal (1): for (int i = 0; i < readings.size(); i++) in part (a) visits indexes 0 through 5 of a six-element list, and for (int i = readings.size() - 1; i >= 0; i--) in part (b) visits 5 down to 0. Neither loop touches an index that does not exist.
  • Accessing elements with get (1): part (a) pulls the element out once into Reading r and then asks it two questions; part (b) chains readings.get(i).getPm25(). Neither reaches for station or pm25 directly, which would not compile from outside Reading.
  • Comparison logic (1): r.getStation().equals(station) compares the characters of the two names. In part (b), < limit is strict, so the 22.0 and 25.0 readings survive removeBelow(15.0) and so would a reading of exactly 15.0.
  • Accumulation (1): sum and count are declared and initialized before the loop, so they survive every pass, and both are updated inside the if, so only North readings contribute to the 12.0 + 25.0 + 18.5 = 55.5 that becomes 18.5.
  • Correct removal without skipping (1): the backward loop plus readings.remove(i) removes the 14.0, the 9.0 and the 12.0 readings in that order and leaves the three survivors in their original relative order, and removed++ counts each one, giving 3.
  • Return (1): part (a) returns -1.0 from inside the count == 0 guard and sum / count only after the loop has seen every reading; part (b) returns removed after the loop, so it reports the total rather than 1 on the first removal.
  • Uses the given classes as specified (1): only size(), get(int) and remove(int) are used on the list, all three on an ArrayList of Reading, and the two Reading accessors do all the data reading.
  • Java that would compile (1): both headers are copied from the question, double sum, int count, Reading r, int removed and both loop variables are declared, and every path out of each method returns a value of the declared type.
  • Design and encapsulation (1): averageFor never calls a mutator, so readings is unchanged as the postcondition requires; removeBelow does its counting in the same pass that does its removing rather than counting first and removing second; and neither method prints anything.

Common ways to lose points. The forward-removal skip is the single most common error on this question type. Written as for (int i = 0; i < readings.size(); i++) with a readings.remove(i) inside, the loop misses any reading that lands in slot i because its predecessor was just deleted. On this list the bug hides at limit 15.0 (the qualifying readings happen not to be adjacent, so the buggy version also returns 3), but at limit 20.0 the correct method returns 4 and leaves South/1/22.0 and North/2/25.0, while the forward version returns 3 and leaves South/1/22.0, North/2/25.0 and North/3/18.5: the 18.5 reading slid into the index the loop had already moved past. Never trust a removal loop that you only tested on data whose targets are spread out.

The second trap is writing part (b) as an enhanced for loop. Removing from a list while a for (Reading r : readings) loop is iterating over it throws java.util.ConcurrentModificationException at run time, which is a real crash and costs the correctness points for the part. The enhanced for is the right tool for part (a), where nothing is modified, and the wrong tool for part (b). Two more point-losers show up every year: comparing station names with ==, which compares references and returns false for two equal names built separately; and returning 0.0 or 0 instead of -1.0 for the no-match case, which throws away the edge-case point even though the rest of the method is perfect. Finally, the design point goes to responses that do one job per method: an averageFor that "helpfully" deletes bad readings, or a removeBelow that prints its count as well as returning it, has side effects the specification never asked for.

FRQ 6 · Data Analysis with ArrayList · 22 minutes · 9 points

Keeping a delivery log sorted and reporting the late orders

Directions

Write your answer to each part in the space below. Your response is scored on Java that would compile: each method header must match the header given in the question exactly, every variable must be declared with a type, and every object you create must use a constructor that exists. Use the given classes as specified: read an Order through its accessor methods rather than its private fields, and use the ArrayList methods size, get and add rather than writing your own. Do not write anything the question does not ask for: a method told to return a value must return it rather than print it, and it must not change data the question does not tell it to change. You have about 22 minutes.

A delivery service records each completed order together with the number of minutes the delivery took. The dispatcher wants the log kept in increasing order of delivery time at all times, so that the slowest deliveries are always at the end and no sorting pass is ever needed, and wants to be able to pull the names of the customers whose orders ran long.

The Order class below is given to you. You will not write any part of it; you will only call its accessor methods.

public class Order
{
    /** The name of the customer who placed this order; never null. */
    private String customer;

    /** The delivery time of this order, in minutes; never negative. */
    private int minutes;

    public Order(String c, int m)
    {
        customer = c;
        minutes = m;
    }

    /** @return the name of the customer who placed this order */
    public String getCustomer()
    {
        return customer;
    }

    /** @return the delivery time of this order, in minutes */
    public int getMinutes()
    {
        return minutes;
    }
}

The class DeliveryLog stores the completed orders. Its list is always kept in increasing order of getMinutes(). You will write two of its methods; their declarations, preconditions and postconditions are shown below.

public class DeliveryLog
{
    /** The completed orders, in increasing order of getMinutes().
     *  Guaranteed not to be null, but it may be empty.
     */
    private ArrayList<Order> orders;

    /** Inserts o into orders so that orders remains in increasing order of
     *  getMinutes(), placing o after every order already in the list whose
     *  minutes are equal to o.getMinutes(), and returns the index at which
     *  o was inserted.
     *  Precondition: o is not null; orders is in increasing order of getMinutes().
     *  Postcondition: orders has one more element than before, it is still in
     *                 increasing order of getMinutes(), and the returned index
     *                 is the position of o in orders.
     */
    public int insertInOrder(Order o)
    {  /* to be implemented in part (a) */  }

    /** Returns a new list containing the customer name of every order in this log
     *  whose delivery time is strictly greater than limit, in the order those
     *  orders appear in orders, keeping repeated names.
     *  Postcondition: orders is unchanged; an empty list is returned when no
     *                 order took more than limit minutes.
     */
    public ArrayList<String> lateCustomers(int limit)
    {  /* to be implemented in part (b) */  }

    // There may be instance variables, constructors, and methods that are not shown.
}

Part (a). Write the method insertInOrder. The method puts o into orders at the one position that keeps the list in increasing order of getMinutes(), and it returns the index where o ended up. When the list already contains one or more orders with exactly the same delivery time, o goes after all of them. Inserting into an empty log puts o at index 0 and returns 0; inserting an order slower than every order already in the log appends it to the end. Write the complete method, beginning with the header public int insertInOrder(Order o).

Part (b). Write the method lateCustomers. The method returns a new ArrayList<String> holding the customer name of every order whose delivery time is strictly greater than limit, in the same order those orders appear in orders. A customer who has two late orders appears twice. An order that took exactly limit minutes is not late. If no order qualifies, the method returns an empty list, not null. The log itself must not change. Write the complete method, beginning with the header public ArrayList<String> lateCustomers(int limit).

In the examples below, log is a DeliveryLog whose orders list starts as Ortiz/22, Chen/35, Ortiz/41, and empty is a DeliveryLog whose orders list is empty. Each call on log is made on the list the row above it left behind.

CallContents of orders beforeValue returnedContents of orders after
log.insertInOrder(new Order("Chen", 35))Ortiz/22, Chen/35, Ortiz/412Ortiz/22, Chen/35, Chen/35, Ortiz/41
log.insertInOrder(new Order("Nawaz", 50))Ortiz/22, Chen/35, Chen/35, Ortiz/414Ortiz/22, Chen/35, Chen/35, Ortiz/41, Nawaz/50
log.lateCustomers(30)Ortiz/22, Chen/35, Chen/35, Ortiz/41, Nawaz/50[Chen, Chen, Ortiz, Nawaz]unchanged
log.lateCustomers(41)Ortiz/22, Chen/35, Chen/35, Ortiz/41, Nawaz/50[Nawaz]unchanged
log.lateCustomers(90)Ortiz/22, Chen/35, Chen/35, Ortiz/41, Nawaz/50[]unchanged
empty.insertInOrder(new Order("Pike", 15))(empty)0Pike/15
Your code
Scoring notes, where the 9 points are
  • +1 Traversal. Part (a) scans orders from index 0 with a bound that stops at orders.size(), so an empty log and a new slowest order both fall out of the scan instead of reading an element that is not there; part (b) visits every element of orders exactly once, from index 0 to orders.size() - 1.
  • +1 Accessing elements with get. Reads each stored order as orders.get(i) and takes its data from the given accessors getMinutes() and getCustomer(), never by naming a field of Order.
  • +1 Comparison logic. Part (a) keeps scanning while the stored order's minutes are less than or equal to o.getMinutes(), which is what places a tie after the orders it ties with; part (b) tests getMinutes() > limit strictly, so an order of exactly limit minutes is not reported.
  • +1 Accumulation. Part (b) creates a new ArrayList<String> before the loop and adds getCustomer() to it on exactly the qualifying passes, so repeated names are kept and the names come out in list order.
  • +1 Correct insertion without skipping or disturbing order. Part (a) calls orders.add(index, o) exactly once, at the index the scan stopped at, and does not remove, re-add or rebuild the list; every element from that index on shifts right by one and the list is still sorted.
  • +1 Return. Part (a) returns the index at which o landed, after the insertion, not from inside the scan, and not printed; part (b) returns the built list after the loop, returning an empty list rather than null when nothing qualifies.
  • +1 Uses the given classes as specified. Uses the ArrayList API correctly (size(), get(int), add(int, Order) for the insertion and add(String) for the result) with the type parameters ArrayList<Order> and ArrayList<String>, and calls the supplied Order methods instead of re-implementing them.
  • +1 Java that would compile. Both headers match public int insertInOrder(Order o) and public ArrayList<String> lateCustomers(int limit) exactly, the result list is created with new ArrayList<String>(), every local variable is declared, the index variable is declared outside the scan so it is still in scope at the return, and braces, parentheses and semicolons balance.
  • +1 Design and encapsulation. No side effects beyond the specification and no unnecessary code: lateCustomers builds a new list and leaves orders completely untouched, insertInOrder changes orders only by the single insertion the question asks for, neither method prints when it is asked to return, neither traverses the list a second time, and no variable is declared that is never used.
Show a 9/9 solution

Part (a).

public int insertInOrder(Order o)
{
    int index = 0;
    while (index < orders.size() && orders.get(index).getMinutes() <= o.getMinutes())
    {
        index++;
    }
    orders.add(index, o);
    return index;
}

Part (b).

public ArrayList<String> lateCustomers(int limit)
{
    ArrayList<String> names = new ArrayList<String>();
    for (int i = 0; i < orders.size(); i++)
    {
        Order o = orders.get(i);
        if (o.getMinutes() > limit)
        {
            names.add(o.getCustomer());
        }
    }
    return names;
}

Part (a) is a scan that stops, not a scan that finishes. The while loop walks index forward as long as two things are true: there is still an element to look at, and that element belongs in front of o. The moment either fails, index is exactly the slot o should occupy, and add(index, o) puts it there and shifts everything from that slot onward one place to the right. The order of the two tests joined by && is not a style choice: && short-circuits, so index < orders.size() must come first or the call orders.get(index) will run on an index past the end. Part (b) is the plainest possible build-a-new-list traversal: declare the result before the loop, look at every order once, append a name when the test passes, return the result after the loop. A driver that builds the example log, makes each call in turn and prints each order as customer/minutes produces exactly the table values:

start                       -> [Ortiz/22, Chen/35, Ortiz/41]
insertInOrder(Chen/35)      -> 2
orders now                  -> [Ortiz/22, Chen/35, Chen/35, Ortiz/41]
insertInOrder(Nawaz/50)     -> 4
orders now                  -> [Ortiz/22, Chen/35, Chen/35, Ortiz/41, Nawaz/50]
lateCustomers(30)           -> [Chen, Chen, Ortiz, Nawaz]
lateCustomers(41)           -> [Nawaz]
lateCustomers(90)           -> []
orders unchanged by (b)     -> [Ortiz/22, Chen/35, Chen/35, Ortiz/41, Nawaz/50]
empty insertInOrder(Pike/15)-> 0
empty orders now            -> [Pike/15]
Where each of the 9 points is earned
  • Traversal (1): the while in part (a) starts at 0 and is guarded by index < orders.size(), so on an empty log it never runs (index stays 0) and on the 50-minute insertion it stops with index equal to 4, the size of the list. The for in part (b) runs i from 0 through 4 on the five-element list.
  • Accessing elements with get (1): part (a) uses orders.get(index).getMinutes() and part (b) pulls the element into Order o once and then asks it for both its minutes and its customer. Neither touches customer or minutes directly, which would not compile outside Order.
  • Comparison logic (1): <= in part (a) is what makes the tie land after the existing 35-minute order and return 2 instead of 1; > limit in part (b) is what keeps the 41-minute order out of lateCustomers(41) while leaving Nawaz in.
  • Accumulation (1): names is created before the loop, so it survives every pass, and names.add(o.getCustomer()) runs only inside the if, which is why lateCustomers(30) returns four names, keeping the repeated Chen, and lateCustomers(90) returns an empty list.
  • Correct insertion without skipping or disturbing order (1): a single orders.add(index, o) does the whole job. The list is never rebuilt, nothing is removed, and the elements at and after index shift right, so Ortiz/41 moves from index 2 to index 3 and the list is still sorted.
  • Return (1): part (a) returns index after the insertion, so the caller learns where the order landed; part (b) returns names after the loop rather than returning on the first match, which is why all four late names come back instead of only the first.
  • Uses the given classes as specified (1): only size(), get(int), add(int, Order) and add(String) are used, each on a list with the right type parameter, and both Order accessors do all the reading.
  • Java that would compile (1): both headers are copied from the question, int index is declared before the while so it is still in scope at the return, names is declared as ArrayList<String> and created with new, and every path out of each method returns a value of the declared type.
  • Design and encapsulation (1): lateCustomers calls no mutator on orders, so the postcondition "orders is unchanged" holds, as the last driver line confirms; insertInOrder performs exactly one insertion; neither method prints; and neither walks the list twice when one pass does the work.

Common ways to lose points. The first is the comparison that decides the tie. Writing the scan as orders.get(index).getMinutes() < o.getMinutes() stops one slot too early on a tie: on the example list that call returns 1 instead of 2 and leaves the new order in front of the 35-minute order already there. The list is still sorted, so the bug is easy to miss, but the returned index is wrong and the specification's "after any order with the same minutes" is broken. The second is dropping the index < orders.size() guard, or putting it after the get. Inserting Nawaz/50 then crashes with java.lang.IndexOutOfBoundsException: Index 3 out of bounds for length 3, and inserting into an empty log crashes with java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0: both of the edge cases the question spells out.

The third is reaching for an enhanced for loop in part (a). An ArrayList may not be structurally modified while a for (Order cur : orders) loop is iterating over it, so a version that calls orders.add(index, o) inside the loop and keeps going throws java.util.ConcurrentModificationException at run time. The enhanced for also hides the index, and the index is the value you have to return, so it is the wrong tool here twice over. The fourth is answering part (b) by emptying orders of the on-time orders and reading off what is left. That is where the forward-removal skip from the sibling question bites: a for (int i = 0; i < orders.size(); i++) loop that removes at i steps over the element that slides into the vacated slot, so on the final list it reports [Chen, Ortiz, Nawaz] for limit 40 instead of the correct [Ortiz, Nawaz], and the same idea written with an enhanced for throws ConcurrentModificationException instead. Even when such a version somehow produces the right names, it destroys the caller's log, which costs the design point outright: the question says to return a new list, so build a new list. Finally, printing the index or the names instead of returning them, or returning null when nothing qualifies, both fail the return point even though the traversal above them is correct.

FRQ 7 · 2D Array · 22 minutes · 9 points

Seating chart

A campus lecture hall tracks its seats in a rectangular grid. The class SeatingChart stores that grid in a private two-dimensional array of int called seats, where seats[r][c] is 0 when the seat in row r, column c is empty and 1 when it is taken. Row 0 is the front row and column 0 is the left-hand aisle. The grid is rectangular: every row has the same number of columns, so seats.length is the number of rows and seats[0].length is the number of columns in each row. The constructor is written for you.

The registrar needs two pieces of information. First, when a block of seats is reserved for a visiting group, the office needs to know how many seats inside that block are already taken. Second, when a late student arrives, the usher needs to know which row has the most room. You will write the two methods that answer those questions. Both methods read the grid; neither one changes it.

public class SeatingChart
{
    /** seats[r][c] is 0 when the seat in row r, column c is empty
     *  and 1 when that seat is taken.
     */
    private int[][] seats;

    /** Constructs a seating chart from initial.
     *  Precondition: initial is rectangular and contains only 0 and 1.
     */
    public SeatingChart(int[][] initial)
    {
        seats = initial;
    }

    /** Returns the number of taken seats inside the rectangle whose opposite
     *  corners are (startRow, startCol) and (endRow, endCol), counting both
     *  corners and every seat between them.
     *  Precondition: 0 <= startRow <= endRow < seats.length
     *                0 <= startCol <= endCol < seats[0].length
     *  Postcondition: seats is not changed.
     */
    public int countTaken(int startRow, int startCol, int endRow, int endCol)
    {
        /* to be implemented in part (a) */
    }

    /** Returns the index of the row that holds the most empty seats.
     *  If two or more rows tie, the smallest such index is returned.
     *  Returns -1 if seats has no rows.
     *  Postcondition: seats is not changed.
     */
    public int emptiestRow()
    {
        /* to be implemented in part (b) */
    }

    // There may be instance variables, constructors, and methods that are not shown.
}

The examples below all refer to a SeatingChart named hall built from this 4-by-5 grid. The row and column labels are the indexes, not part of the data.

col 0col 1col 2col 3col 4
row 010101
row 101101
row 200110
row 311011
CallValue returnedWhy
hall.countTaken(1, 1, 2, 3)4Rows 1 and 2, columns 1 through 3: the 1s at (1,1), (1,2), (2,2) and (2,3).
hall.countTaken(0, 0, 3, 4)12The whole hall: 3 + 3 + 2 + 4 taken seats.
hall.countTaken(0, 2, 0, 2)1A one-seat rectangle. Both corners are the same seat, and it is taken.
hall.countTaken(3, 0, 3, 4)4All of row 3, which holds four taken seats and one empty seat.
hall.emptiestRow()2Empty counts by row are 2, 2, 3, 1, so row 2 has the most room.

Part (a). Write the method countTaken. It returns how many taken seats lie inside the rectangle whose opposite corners are row startRow column startCol and row endRow column endCol. Both corners count, and so does every seat between them, so a rectangle from row 1 to row 2 covers rows 1 and 2. Precondition: all four values are legal indexes, startRow is at most endRow, and startCol is at most endCol. Postcondition: seats is unchanged. Complete the method below.

public int countTaken(int startRow, int startCol, int endRow, int endCol)

Part (b). Write the method emptiestRow. It returns the index of the row holding the most empty seats. If two or more rows are tied for the most empty seats, return the smallest of those indexes. If the chart has no rows at all, return -1. There is no precondition on the number of rows, so seats.length may be 0; you may assume that when there is at least one row the grid is rectangular. Postcondition: seats is unchanged. Complete the method below.

public int emptiestRow()
Directions

You have 22 minutes and 9 points. Write your answer in Java, as a method body that would compile if it were dropped into the SeatingChart class exactly as shown. Use the class as it is given: seats is the instance variable you read, and you may not add parameters to a header or change a header that the question supplies. Size your loops from the array itself with seats.length and seats[0].length rather than the numbers 4 and 5, because the grader will run your code on charts of other sizes. Do not print anything; each method returns a value. Do not modify seats. Partial credit is real, so write the loop you are sure of even if an edge case is still troubling you, but code that cannot compile earns nothing in that part.

Your code
Scoring notes · 9 points (6 correctness · 2 Java API and syntax · 1 design)
  • +1 declares and initializes the local variables before the loops that use them: a counter set to 0 in countTaken, and in emptiestRow both a best-row index and a most-empty-so-far value, with the per-row empty counter reset to 0 at the top of every row.
  • +1 uses a nested loop with correct bounds in countTaken: the outer loop runs from startRow while the index is at most endRow, the inner loop from startCol while the index is at most endCol. The tests are at-most, not less-than, so the last row and last column of the rectangle are counted.
  • +1 traverses every row and every column of the chart in emptiestRow, the outer loop over rows and the inner loop over columns, visiting each seat exactly once.
  • +1 tests the condition the specification states: counts a seat in countTaken when its value is 1, and counts a seat in emptiestRow when its value is 0.
  • +1 accumulates correctly: increments the rectangle counter on every taken seat, and after each row compares that row's empty count against the best seen so far, storing both the new count and the row index when it is larger.
  • +1 returns the right value at the right time: the rectangle count after both loops finish, not inside them; the best row index after the outer loop; and -1 before any loop runs when seats.length is 0. Ties keep the smaller index because the comparison is strictly greater, never greater-or-equal.
  • +1 uses two-dimensional array syntax correctly: seats[r][c] with the row index first and the column index second, and seats.length for the number of rows and seats[0].length for the number of columns, each written with no parentheses.
  • +1 writes Java that would compile: both headers copied exactly as given and returning int, every local variable declared with a type, every loop variable used inside its own scope, and braces and semicolons balanced.
  • +1 design and encapsulation: neither method assigns to seats or to any of its elements, neither prints instead of returning, each traverses only the data it needs one time, no variable is declared and never used, and seats stays private and is read directly only inside SeatingChart.
Show a 9/9 solution

Part (a). The rectangle is given by its two corners, so the loop variables start at the corner indexes and the tests are <=. Nothing else about the grid matters.

public int countTaken(int startRow, int startCol, int endRow, int endCol)
{
    int count = 0;
    for (int r = startRow; r <= endRow; r++)
    {
        for (int c = startCol; c <= endCol; c++)
        {
            if (seats[r][c] == 1)
            {
                count++;
            }
        }
    }
    return count;
}

Part (b). Here the whole chart is traversed. The outer loop walks the rows; the inner loop counts the empty seats in one row; after the inner loop finishes, that row's count is compared against the best seen so far. Starting mostEmpty at -1 guarantees that row 0 replaces it, even in a chart where every seat is taken and every row has zero empty seats.

public int emptiestRow()
{
    if (seats.length == 0)
    {
        return -1;
    }
    int best = 0;
    int mostEmpty = -1;
    for (int r = 0; r < seats.length; r++)
    {
        int empty = 0;
        for (int c = 0; c < seats[0].length; c++)
        {
            if (seats[r][c] == 0)
            {
                empty++;
            }
        }
        if (empty > mostEmpty)
        {
            mostEmpty = empty;
            best = r;
        }
    }
    return best;
}

Compiled and run on the 4-by-5 chart above, the two methods print exactly the values in the examples table, plus the per-row empty counts that explain part (b):

countTaken(1, 1, 2, 3) -> 4
countTaken(0, 0, 3, 4) -> 12
countTaken(0, 2, 0, 2) -> 1
countTaken(3, 0, 3, 4) -> 4
emptiestRow()          -> 2
-- empty seats per row --
row 0: 2 empty
row 1: 2 empty
row 2: 3 empty
row 3: 1 empty
tie chart 1 0 / 0 1 / 1 1, emptiestRow() -> 0
empty chart, emptiestRow() -> -1
Where the 9 points are earned
  • Correctness 1: local variables: int count = 0; sits before the nested loop, and best, mostEmpty and the per-row empty are each declared where they belong. empty is declared inside the outer loop, which is what resets it to zero at the start of every row.
  • Correctness 2: bounds: r = startRow; r <= endRow and c = startCol; c <= endCol. The rectangle is inclusive at both ends, so both tests are at-most.
  • Correctness 3: full traversal: emptiestRow runs r from 0 to seats.length - 1 and c from 0 to seats[0].length - 1, so every seat is examined once and no row is skipped.
  • Correctness 4: the test: seats[r][c] == 1 for taken in part (a) and seats[r][c] == 0 for empty in part (b). Reversing these is the single most common way to lose this point.
  • Correctness 5: accumulation: count++ on each taken seat; empty++ on each empty seat; and the update of mostEmpty and best together, so the stored index always belongs to the stored count.
  • Correctness 6: returns and edge cases: return count; is after both loops, return best; is after the outer loop, and the guard if (seats.length == 0) return -1; handles the no-rows chart before seats[0] is ever evaluated. The strict > keeps the smaller index on a tie: the run above shows a tied chart returning 0.
  • Java API 1: array syntax: every access is seats[r][c], row first and column second; the sizes come from seats.length and seats[0].length, written without parentheses, so the methods work on a chart of any rectangular size.
  • Java API 2: it compiles: both headers match the ones in the class exactly, every variable has a declared type, and each loop variable is used only inside its own loop.
  • Design 1, no side effects: neither method writes to seats, neither prints, each makes one pass over the data it needs, and no unused variable is declared. seats remains private and is touched only from inside its own class.

Common ways to lose points. The first is the off-by-one: writing r < endRow and c < endCol out of habit. That drops the last row and last column of the rectangle, and the compiler will never complain. Run on the chart above it turns countTaken(1, 1, 2, 3) into 2 and countTaken(0, 0, 3, 4) into 6: both wrong, and it costs the bounds point. The second is swapping the indexes, writing seats[c][r] inside the column loop. On a square grid that quietly returns the wrong answer; on this 4-by-5 chart the column index reaches 4 and the run ends in java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4. The third is confusing the two lengths: seats.length is the number of rows and seats[0].length is the number of columns, so using seats.length as the column bound is another out-of-bounds crash on any chart that is not square. A fourth, quieter mistake is initializing mostEmpty to 0 instead of -1 and forgetting the no-rows guard: in a completely full hall every row ties at zero empty seats, and an accidental >= comparison would then return the last row instead of the first, losing the edge-case point.

FRQ 8 · 2D Array · 22 minutes · 9 points

Rainfall grid

A weather station logs its rainfall readings in a rectangular grid. The class RainfallGrid stores that grid in a private two-dimensional array of int called data. Each row is one week and each column is one of the fixed collection days in that week, so data[r][c] is the rainfall recorded in week r on collection day c, measured in whole millimetres and never negative. The grid is rectangular: data.length is the number of weeks and data[0].length is the number of collection days per week. The constructor is written for you.

The station's report needs two summaries. The first is a day-by-day total: how much rain fell on collection day 0 across all the weeks, on day 1 across all the weeks, and so on. That summary is a one-dimensional array with one slot per column, which means the traversal has to run down a column before it moves to the next one. The second summary counts peaks: cells that are strictly wetter than every neighbor directly above, below, left and right of them. A cell in the top row has no neighbor above it, a cell in the left column has none to its left, and a corner cell has only two neighbors: a peak is judged only against the neighbors it actually has.

public class RainfallGrid
{
    /** data[r][c] is the rainfall recorded in week r on collection day c.
     *  Every value is at least 0.
     */
    private int[][] data;

    /** Constructs a rainfall grid from initial.
     *  Precondition: initial is rectangular and contains only values >= 0.
     */
    public RainfallGrid(int[][] initial)
    {
        data = initial;
    }

    /** Returns a new one-dimensional array whose element at index c is the
     *  sum of all the values in column c of data.
     *  Returns an array of length 0 when data has no rows.
     *  Postcondition: data is not changed.
     */
    public int[] columnTotals()
    {
        /* to be implemented in part (a) */
    }

    /** Returns the number of cells of data whose value is strictly greater
     *  than the value of every neighbour that exists directly above, below,
     *  to the left of, and to the right of that cell. Diagonal cells are not
     *  neighbours. A cell on an edge or in a corner simply has fewer
     *  neighbours to beat.
     *  Postcondition: data is not changed.
     */
    public int countPeaks()
    {
        /* to be implemented in part (b) */
    }

    // There may be instance variables, constructors, and methods that are not shown.
}

The examples below refer to a RainfallGrid named season built from this 3-by-4 grid, and to three smaller grids: oneWeek, which holds the single row 5 2 9; flat, which holds the two rows 4 4 and 1 2; and blank, which has no rows at all. The row and column labels are the indexes, not part of the data.

col 0col 1col 2col 3
row 03825
row 14691
row 27253
CallValue returnedWhy
season.columnTotals()an array holding 14, 16, 16, 9Column 0 is 3 + 4 + 7, column 1 is 8 + 6 + 2, column 2 is 2 + 9 + 5, column 3 is 5 + 1 + 3.
season.countPeaks()4The cells holding 8, 5, 9 and 7: that is (0,1), (0,3), (1,2) and (2,0).
oneWeek.columnTotals()an array holding 5, 2, 9With one row, each column total is that row's single value.
oneWeek.countPeaks()25 beats its only neighbor 2, and 9 beats its only neighbor 2. The 2 in the middle beats neither.
flat.countPeaks()0The two 4s are tied with each other, and a tie is not strictly greater, so neither one is a peak.
blank.columnTotals()an array of length 0A grid with no rows has no columns to total.

Part (a). Write the method columnTotals. It returns a new one-dimensional array whose element at index c is the sum of every value in column c of data. The returned array must have exactly one slot per column. Precondition: none beyond the class invariant; data.length may be 0, and in that case the method returns an array of length 0. Postcondition: data is unchanged. Complete the method below.

public int[] columnTotals()

Part (b). Write the method countPeaks. It returns how many cells of data are strictly greater than every neighbor that exists directly above, below, to the left of and to the right of that cell. Diagonals are not neighbours, a cell on an edge is judged only against the neighbors it has, and a cell tied with any one of its neighbors is not a peak. Precondition: none beyond the class invariant. Postcondition: data is unchanged. Complete the method below.

public int countPeaks()
Directions

You have 22 minutes and 9 points. Write your answer in Java, as method bodies that would compile if they were dropped into the RainfallGrid class exactly as shown. Use the class as it is given: data is the instance variable you read, and you may not change a header the question supplies or add parameters to it. Take every bound from the array itself: data.length for weeks, data[0].length for collection days, because the grader will run your code on grids of other sizes. Do not print anything; part (a) returns an array and part (b) returns an int. Do not modify data or any of its elements. Partial credit is real, so write the traversal you are sure of even if one bounds guard is still troubling you, but code that cannot compile earns nothing in that part.

Your code
Scoring notes · 9 points (6 correctness · 2 Java API and syntax · 1 design)
  • +1 declares and initializes before looping: creates the result array with new int[data[0].length] (one slot per column, not one per row), and sets a running sum to 0 at the start of each column, and a peak counter to 0 before the traversal in part (b).
  • +1 uses a column-major nested loop with correct bounds in columnTotals: the outer loop over columns while the index is less than data[0].length, the inner loop over rows while the index is less than data.length, so every cell of every column is added and no first or last column is missed.
  • +1 accumulates correctly: adds data[r][c] into the running sum on every pass of the inner loop, stores that sum at totals[c] once the column is finished, and increments the peak counter exactly once per qualifying cell in part (b).
  • +1 guards all four neighbor accesses with a bounds test evaluated before the element is read: r - 1 >= 0, r + 1 < data.length, c - 1 >= 0 and c + 1 < data[0].length, so an edge or corner cell is judged only against the neighbors that exist and no access runs off the grid.
  • +1 compares strictly: a cell counts only when its value is greater than each existing neighbor, so a cell tied with a neighbor is disqualified and a greater-or-equal test does not earn this point.
  • +1 returns the right value of the right type at the right time: the totals array after the loops finish rather than inside them, an array of length 0 when data.length is 0, and the peak count after the whole grid has been traversed.
  • +1 uses array syntax and the Java API correctly: data[r][c] with the row index first and the column index second, data.length and data[0].length written with no parentheses, and the result array created with new int[…] at a length taken from the grid rather than a hard-coded 3 or 4.
  • +1 writes Java that would compile: both headers copied exactly as given, part (a) declared to return int[] and part (b) int, every local variable declared with a type, each loop variable used only inside its own scope, and braces and semicolons balanced.
  • +1 design and encapsulation: builds and returns a new array instead of writing into data, changes no element of the grid, prints nothing, makes a single pass for each summary rather than re-traversing, declares no variable it never uses, and keeps data private and read only from inside RainfallGrid.
Show a 9/9 solution

Part (a). The shape of the answer decides the shape of the loop. One result per column means the column index is the one that advances on the outside, and the row index runs down the column on the inside. The guard for the empty grid comes first, because data[0].length cannot be evaluated on a grid with no rows.

public int[] columnTotals()
{
    if (data.length == 0)
    {
        return new int[0];
    }
    int[] totals = new int[data[0].length];
    for (int c = 0; c < data[0].length; c++)
    {
        int sum = 0;
        for (int r = 0; r < data.length; r++)
        {
            sum += data[r][c];
        }
        totals[c] = sum;
    }
    return totals;
}

Part (b). Here the traversal order does not matter, every cell is tested, but each of the four neighbor comparisons must be protected by its own bounds test. Writing the test as "assume it is a peak, then look for a reason it is not" keeps the four guards independent, and because Java evaluates && left to right, the bounds test always runs before the array access it protects.

public int countPeaks()
{
    int count = 0;
    for (int r = 0; r < data.length; r++)
    {
        for (int c = 0; c < data[0].length; c++)
        {
            boolean peak = true;
            if (r - 1 >= 0 && data[r][c] <= data[r - 1][c])
            {
                peak = false;
            }
            if (r + 1 < data.length && data[r][c] <= data[r + 1][c])
            {
                peak = false;
            }
            if (c - 1 >= 0 && data[r][c] <= data[r][c - 1])
            {
                peak = false;
            }
            if (c + 1 < data[0].length && data[r][c] <= data[r][c + 1])
            {
                peak = false;
            }
            if (peak)
            {
                count++;
            }
        }
    }
    return count;
}

Compiled and run on the four grids named in the question, the two methods produce exactly the values in the examples table:

columnTotals() -> [14, 16, 16, 9]
countPeaks()   -> 4
-- which cells are peaks --
peak at (0, 1) value 8
peak at (0, 3) value 5
peak at (1, 2) value 9
peak at (2, 0) value 7
one-row grid 5 2 9: columnTotals() -> [5, 2, 9], countPeaks() -> 2
tied grid 4 4 / 1 2: countPeaks() -> 0
empty grid: columnTotals().length -> 0, countPeaks() -> 0
Where the 9 points are earned
  • Correctness 1: declaration and array creation: new int[data[0].length] gives one slot per column, and int sum = 0; is declared inside the outer loop so it resets at the top of every column. The peak counter is initialized to 0 before the traversal.
  • Correctness 2: column-major bounds: the outer loop is c < data[0].length and the inner is r < data.length. Getting these two the wrong way round is the defining error of this question type.
  • Correctness 3: accumulation: sum += data[r][c]; on each pass down the column, then totals[c] = sum; exactly once when that column finishes; count++ once per peak.
  • Correctness 4: the four bounds guards: r - 1 >= 0, r + 1 < data.length, c - 1 >= 0 and c + 1 < data[0].length, each joined to its comparison with &&, so the guard is evaluated first and the element is never read out of bounds. The value 5 at (0,3) is a peak precisely because it has no neighbor above it and none to its right.
  • Correctness 5: strictly greater: the disqualifying test is <=, which means a cell counts only when it is strictly greater than each existing neighbor. On the grid 4 4 / 1 2 the run above returns 0, which is the behavior this point is checking.
  • Correctness 6: returns and the empty grid: return totals; comes after the loops, return count; after the whole traversal, and return new int[0]; handles the no-rows grid before data[0] is ever evaluated.
  • Java API 1: array syntax: every access is data[r][c], row first and column second, even in part (a) where the column index is the outer loop variable; length is written with no parentheses; the new array's size comes from the grid.
  • Java API 2: it compiles: public int[] columnTotals() and public int countPeaks() match the given headers exactly, totals, sum, count and peak are all declared with types, and peak is declared inside the inner loop so it is fresh for every cell.
  • Design 1, no side effects: part (a) returns a brand-new array and never assigns to data[r][c]; neither method prints; each makes one pass over the grid; no unused variable is declared; and data stays private, read only from inside its own class.

Common ways to lose points. The first is confusing the two lengths. data.length is the number of rows and data[0].length is the number of columns, so new int[data.length] builds an array of 3 slots for a grid that has 4 columns; the fourth totals[c] = sum; then ends the run with java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3. The second is swapping the roles of the indexes: looping row-major and storing at totals[r] instead of totals[c] compiles cleanly and quietly returns [18, 20, 17, 0], the three row sums followed by an untouched zero, so only running the examples catches it. The third is dropping the bounds guards in part (b) and reading data[r - 1][c] on the top row, which fails immediately with java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3; the related off-by-one is writing r + 1 <= data.length, which lets the last row reach one past the end. The fourth is the tie: using < instead of <= as the disqualifier turns the test into greater-or-equal and reports 2 peaks on the grid 4 4 / 1 2 where the correct answer is 0. Note that this last error is invisible on the 3-by-4 sample grid, which has no ties and still returns 4 either way: a reminder that passing the given examples is not the same as being correct.

Unit recap

Unit recap

0:00 / 0:00

Animated recap with on-screen narration. Turn on Voice to have it read aloud (uses your device's built-in voice). Pressing play counts as your one free video.

Free preview complete

That's the end of the free preview.

You've opened five lessons (or watched a unit video), which is as much as we can show without a subscription. Everything you've already opened stays available; use the outline on the left to go back to it.

Book a tutor instead