Module 1: From Nothing to a Running Java Program
Two lessons that take you from an empty file to a program you have compiled, run, broken on purpose, and read the compiler's complaint about. The first covers the shape of every Java program and the two commands that turn it into output; the second covers the primitive types and the arithmetic that catches people out, including 7 / 2 and the int that wraps around to a negative number.
Hello, compiler: your first Java program, word by word
- Write, compile and run a Java program that prints text.
- Name the job of every word in the class header and the main method header.
- Read the three compiler and run-time errors a beginner meets first.
Five lines, one command, two lines of output
Here is a complete Java program. It is six lines long, it was saved in a file named Hello.java, and it really was compiled and run on the machine this course was written on.
public class Hello {
public static void main(String[] args) {
System.out.println("Ada Lovelace");
System.out.println("This program is running.");
}
}
Running it printed exactly this:
Ada Lovelace
This program is running.
Two instructions in, two lines out, in the order they were written. That is the whole behaviour of the program, and everything else in this lesson is an answer to the question you should be asking: why does it take all those words to print two lines? Python needs one line for this. Java needs six. The extra words are not ceremony you have to memorise and resent. Each one is a decision the language is forcing you to state out loud, and by the end of this lesson you will be able to say what each of them decides.
A note about the tools before we start. Java is a large language that has been growing since 1995, and the version used to run every program in this course is Java 21. The AP Computer Science A exam does not test all of Java. It tests a subset, and the library methods you are allowed to assume are printed on a single handout called the AP Java Quick Reference, which the College Board includes in the digital testing application on exam day: String, Math, Integer, Double, ArrayList, File and Scanner for reading a text file, and the equals and toString methods that every object has. This course stays inside that subset on purpose. When Java 21 offers a shortcut the exam does not recognise, this course does it the exam's way and says so. That is the only time you will hear about version numbers.
Line one: public class Hello
Every piece of Java code lives inside a class. There is no such thing as a loose statement floating in a Java file; the class is the container, and the first line declares one.
public class Hello {
Read it right to left. The name is Hello. Class names in Java start with a capital letter by convention, and if the name has several words you run them together with each word capitalised, as in BankAccount or GridWorld. The word class says what kind of thing is being declared. The word public says that any other code may use this class, as opposed to only code in the same folder.
Then comes an opening brace, {, and at the very bottom of the file a matching closing brace. Everything between them belongs to the class. Braces in Java are like the walls of a room: they group statements, and every opening brace must have a partner. A missing brace is one of the two errors you will make most often in your first month, and the compiler's message for it is rarely the line you expected, because the compiler only notices something is wrong when it runs off the end of the file.
One rule connects the class to the file on disk, and it has no exceptions: a public class must be saved in a file with exactly its own name plus .java. Class Hello goes in Hello.java. Capitalisation counts. You will see the compiler enforce this later in the lesson, in the least forgiving way it knows.
Line two: the eleven words that start the program
public static void main(String[] args) {
When you tell the computer to run your class, it looks for one particular method to start at, and that method must be spelled exactly like the line above. It is called main. The Java tutorial at Oracle puts it plainly: every application must contain a main method whose signature is public static void main(String[] args), and it is the entry point for the application.
Take the words one at a time, because you will write this line hundreds of times and you should know what you are saying.
public: the machinery that starts your program lives outside your class, so it needs permission to call this method.static: the method belongs to the class itself, not to an object made from the class. Nothing has been created yet when the program starts, so the starting method cannot need an object to exist. Static is meaningless to you now; in Lesson 9 it will be one of the most useful words you know.void: this method hands nothing back to whoever called it. Many methods return a value, such as a number or a piece of text. This one returns nothing at all, andvoidis how you say that.main: the name. The name is not a suggestion. A method calledMainorstartwill compile perfectly and then refuse to run.(String[] args): the information main receives, namely a list of text values typed after the program's name on the command line. You will not use it in this course, but it has to be in the signature, because that is the signature the launcher looks for.
Key idea: the class header says what this file is; the main header says where execution begins. Those are the two questions every Java file has to answer before it can do anything.
Saying something out loud
Inside main are the statements that actually do the work, and each one ends with a semicolon. The semicolon is not decoration: it tells the compiler that one instruction has finished. Java ignores line breaks and indentation entirely, so the semicolon is the only thing marking where a statement stops.
The statement that prints is System.out.println(...). Read it as three parts: System is a class supplied with Java, out is the standard output stream it holds, and println is the method you are calling on that stream. The text in double quotes is a String literal, a fixed piece of text. Here is a second program that pushes on printing a little harder. It was compiled and run.
public class Printing {
public static void main(String[] args) {
System.out.print("2 + 2 = ");
System.out.println(2 + 2);
System.out.print("no line break after me");
System.out.println(" ...so this joins on");
System.out.println("She said \"compile it\" and left.");
System.out.println("A" + "B" + 1 + 2);
System.out.println(1 + 2 + "AB");
}
}
Its real output:
2 + 2 = 4
no line break after me ...so this joins on
She said "compile it" and left.
AB12
3AB
Five things happened there, and all five are worth your attention.
First, print and println differ in one respect: println moves to a new line when it is done, print leaves the cursor where it is. That is why the first two statements produced one line rather than two.
Second, println(2 + 2) printed 4, not "2 + 2". Whatever you put inside the parentheses is worked out first, then printed. The quotes in the earlier statement are what stopped Java from working out the text.
Third, the line with the quotations inside it. A double quote would normally end the String, so to put one inside you write a backslash in front of it. That pair is called an escape sequence. The useful ones for this course are the escaped quote, the escaped backslash, and n after a backslash, which means a new line.
Fourth and fifth, the two lines at the end are the same four values in a different order and they printed different things: AB12 and 3AB. The plus sign in Java does two jobs. Between two numbers it adds. Between a String and anything else it joins, producing a String. Java evaluates left to right, so in the first line "A" + "B" makes "AB", then "AB" + 1 makes "AB1", then + 2 makes "AB12". In the second line 1 + 2 are both numbers, so they add to 3, and only then does the String arrive, giving "3AB". This exact trick appears on the exam most years, and the fix for it is a pair of parentheses: write "total: " + (3 + 4) when you mean the sum.
Two steps, not one: javac and java
Your program made its journey to the screen in two stages, and knowing the two stages is what lets you read an error message.
The first stage is compiling. The compiler, a program called javac, reads Hello.java, checks it against every rule of the language, and if it is satisfied writes a new file called Hello.class. That file does not contain English and it does not contain instructions for your particular processor. It contains bytecode, a compact instruction set designed for an imaginary machine.
The second stage is running. The command java starts the Java Virtual Machine, which reads the bytecode and carries it out on your real processor. This is the reason a compiled Java class file runs on a Windows laptop, a Mac and an Android phone without being changed: each of those has its own virtual machine, and the bytecode is the same everywhere.
The practical consequence is a distinction you must keep sharp for the rest of the course. A compile-time error means javac refused the file. Nothing ran, no output appeared, no class file exists. A run-time error means the file compiled, the program started, and it failed part of the way through, usually after printing some output. When something goes wrong, your first question is always which of the two happened, because they have completely different causes.
Three errors, and what the compiler actually said
The fastest way to stop fearing error messages is to cause them deliberately while nothing is at stake. Here are three, each produced by really running the broken file.
The first is a missing semicolon. This file was saved as Broken.java:
public class Broken {
public static void main(String[] args) {
System.out.println("this line is fine");
System.out.println("this one is missing something")
}
}
The compiler said:
Broken.java:4: error: ';' expected
System.out.println("this one is missing something")
^
1 error
Read it as four pieces of information: the file, the line number, what was expected, and a caret pointing at the exact character position. Note that the caret sits after the closing parenthesis, which is where the semicolon should have been, not at the start of the line. Note too that the first println never ran. Compilation failed, so no part of the program ran.
The second error is the file-name rule from earlier. A file called Greeting.java was given this content:
public class Welcome {
public static void main(String[] args) {
System.out.println("hello from the wrong file name");
}
}
The compiler's reply is unusually helpful:
Greeting.java:1: error: class Welcome is public, should be declared in a file named Welcome.java
public class Welcome {
^
1 error
The third error is different in kind, and it is the one that teaches the compile-and-run distinction best. This class compiled without a single complaint:
public class NoMain {
public static void greet() {
System.out.println("I am never called.");
}
}
Running it produced:
Error: Main method not found in class NoMain, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
Nothing about that class breaks a rule of the language, so javac had no objection. The complaint came from the launcher at run time, when it went looking for a starting point and found none. If you ever see a message beginning with the word Error rather than a file name and a line number, you are past the compiler and into the running program.
Comments, and writing for the next reader
Two forms of comment exist, and the compiler ignores both. Everything after two forward slashes on a line is a comment. Everything between a slash-star and a star-slash is a comment, even across many lines.
// This prints the name at the top of the card.
System.out.println("Rosa Mendez");
/* A block comment can run
over as many lines as you like. */
On the free-response section of the exam, comments earn you nothing directly. What they do is keep you from losing the thread of your own method halfway through writing it under time pressure, which is worth more than it sounds.
Common misconceptions
- "The file name is up to me." It is not, for a public class. Class
Welcomein Greeting.java is a compile error, as the message above shows, and the name has to match down to the capital letter. - "An error message means my whole program is wrong." A compiler error names one line and often one character. The missing semicolon above broke a program that was otherwise perfect. Fix the first error, then compile again: later errors are frequently just the first one echoing.
- "Java cares about my indentation." It does not. You could write the whole Hello program on one line and it would compile. Indentation is entirely for humans, which is exactly why you should be strict about it: the reader of your free-response answer is a human with a stack of other answers to get through.
- "println and print are interchangeable." They differ by one newline, which is enough to turn an expected two-line output into one line and lose you a point on a question about output.
The short version
You now have the whole skeleton. A Java file holds a class; the class holds methods; one method named main is where running starts; each statement inside ends with a semicolon; and printing happens through System.out.print and System.out.println, with the plus sign joining text and adding numbers depending on what sits either side of it. Getting from the file to the screen takes two steps, compiling and running, and every error you meet belongs to one step or the other.
Worth holding on to: the compiler is not your opponent. It is the only reader you have that checks every rule, every time, and its messages give you a file, a line, a column and a name for what it expected. Learning to read four lines of compiler output is worth more this month than memorising any amount of syntax.
Next lesson gives your programs a memory. You will declare variables of each primitive type, and meet the two arithmetic results that surprise every beginner: why 7 divided by 2 is 3, and how an int can be made to turn negative by adding one to it.
Sources
- Oracle. (n.d.). The Java Tutorials: A Closer Look at the Hello World Application (class declaration, the main method signature, and System.out.println). docs.oracle.com
- College Board. (2025). AP Computer Science A: The Exam (the exam is delivered in the Bluebook application and the Java Quick Reference listing the accessible library methods is included in it). apcentral.collegeboard.org
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class System (the out field and the print and println methods). docs.oracle.com
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 1: Computer Programming. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Class
- The container every piece of Java code lives in; a public class must be saved in a file with the same name plus .java.
- Method
- A named block of statements inside a class. main is the one the launcher starts at.
- Statement
- A single instruction, ended by a semicolon. Java ignores line breaks, so the semicolon is what marks the end.
- String literal
- Fixed text written between double quotes, such as the words printed by the Hello program.
- Escape sequence
- A backslash plus a character, used to put a quote, a backslash or a newline inside a String.
- Compile-time error
- A rule of the language broken, reported by javac with a file, line and column. Nothing runs.
- Run-time error
- A failure after the program has started, reported by the running program, often after some output.
- Bytecode
- The instruction set javac writes into a .class file, carried out by the Java Virtual Machine.
Primitive types, expressions, and the answers you did not expect
- Declare and initialise variables of type int, double and boolean.
- Predict the result of integer division, the remainder operator and a cast.
- Explain why an int can wrap round to a negative number and why 0.1 + 0.2 is not 0.3.
Seven slices, two people, and an answer that is wrong
This program divides seven slices of pizza between two people. Read it and decide what you expect before you look at the output.
public class Surprise {
public static void main(String[] args) {
int slices = 7;
int people = 2;
System.out.println("each gets " + slices / people);
System.out.println("left over " + slices % people);
System.out.println("as a decimal " + (double) slices / people);
System.out.println(0.1 + 0.2);
int big = 2147483647;
System.out.println(big + 1);
}
}
Compiled and run, it printed:
each gets 3
left over 1
as a decimal 3.5
0.30000000000000004
-2147483648
Three of those five lines are surprising the first time. Seven divided by two came out as 3. Adding a tenth to two tenths gave a number with a tail of zeros and a stray 4. And adding one to a large positive number produced a large negative one. None of these is a bug in Java, and none is a quirk you can ignore, because the exam asks about all three. Each has the same cause: in Java, every value has a type, and the type decides what the operator does.
Declaring a variable means promising a type
A variable is a named box in memory that holds a value. In Java you must say what kind of value before you may use it, which is what the first word of a declaration does.
int slices = 7;
double price = 4.99;
boolean passing = true;
Read each line as three parts: the type, the name, and the value it starts with. The type is a promise that binds for the whole life of the variable. A variable declared int can never later hold 4.99, and the compiler will stop you rather than quietly losing the decimal part. That is what happened to this file:
public class Lossy {
public static void main(String[] args) {
int x = 3.5;
System.out.println(x);
}
}
Lossy.java:3: error: incompatible types: possible lossy conversion from double to int
int x = 3.5;
^
1 error
The phrase to hold on to is possible lossy conversion. Java will move a value into a wider type without complaint, because nothing is lost: double whole = 5; compiles and stores 5.0. Going the other way loses information, so Java insists that you say in writing that you meant it.
Names follow one hard rule and one convention. The rule: a name may use letters, digits, underscore and dollar, may not begin with a digit, and may not be a reserved word such as class or int. The convention, which the free-response readers expect and which you should follow from today: start with a lowercase letter and capitalise each later word, as in totalScore or numStudents.
The three types that do nearly all the work
Java has eight primitive types, but three of them carry this course, and they are the three the exam leans on.
| Type | Holds | Range or precision | Example literal |
|---|---|---|---|
int | Whole numbers, positive or negative | -2147483648 to 2147483647 | 42, -7, 0 |
double | Numbers with a fractional part | About 15 to 16 significant digits | 4.99, -0.5, 3.0 |
boolean | A truth value, nothing else | Exactly two values | true, false |
Oracle's tutorial states the int range precisely: an int is a 32-bit signed two's complement integer with a minimum of minus two to the 31st power and a maximum of two to the 31st power minus one. A double is a 64-bit IEEE 754 floating-point number, and the same page warns that it should never be used for exact values such as currency. Hold on to that warning; it comes back in two sections.
A fourth type, char, holds a single character. In this course it appears in one place only, as the value charAt hands back when you walk through a String, so it is introduced there rather than here.
Here is a program that simply asks Java to print what it knows about these types. Its output is the evidence for the table above.
public class Types {
public static void main(String[] args) {
int count = 42;
double price = 4.99;
boolean passing = true;
System.out.println(count);
System.out.println(price);
System.out.println(passing);
System.out.println("int runs from " + Integer.MIN_VALUE + " to " + Integer.MAX_VALUE);
System.out.println("one third as a double: " + 1.0 / 3.0);
double whole = 5;
System.out.println("an int put into a double: " + whole);
System.out.println("largest double: " + Double.MAX_VALUE);
}
}
42
4.99
true
int runs from -2147483648 to 2147483647
one third as a double: 0.3333333333333333
an int put into a double: 5.0
largest double: 1.7976931348623157E308
Notice three details in that output. The double 5 printed as 5.0, because its type is part of what it is, not just how it looks. One third printed with sixteen digits and then stopped, because that is where the storage runs out. And the largest double printed in scientific notation, E308 meaning times ten to the power 308.
Integer division, and the remainder that goes with it
Now the first surprise. When both sides of a division are ints, Java does integer division: it divides and throws away anything after the decimal point. It does not round. So 7 / 2 is 3, and 99 / 100 is 0.
Its partner is %, the remainder operator, often called mod. It gives what is left over: 7 % 2 is 1. The pair always satisfies a simple relationship, which is worth checking with numbers: (a / b) * b + (a % b) gives back a. For 7 and 2 that is 3 times 2 plus 1, which is 7.
Together they are the tool for splitting a quantity into units and a remainder, and you will use them constantly: seconds into hours, minutes and seconds; an index into a row and a column; testing whether a number is even by asking whether n % 2 == 0; taking the last digit of a number with n % 10.
public class Split {
public static void main(String[] args) {
int total = 3725;
int hours = total / 3600;
int minutes = total % 3600 / 60;
int seconds = total % 60;
System.out.println(total + " s = " + hours + " h " + minutes + " m " + seconds + " s");
total = 59;
hours = total / 3600;
minutes = total % 3600 / 60;
seconds = total % 60;
System.out.println(total + " s = " + hours + " h " + minutes + " m " + seconds + " s");
}
}
3725 s = 1 h 2 m 5 s
59 s = 0 h 0 m 59 s
The same six lines produced a sensible answer for both inputs, and the second run is the interesting one: with 59 seconds every division gave zero and the remainder kept the whole amount. Integer division is not a defect to be worked around. When you want whole units, it is exactly the operator you want.
Casting: changing the type on purpose
When you do want the decimal, you change one of the operands to a double. Writing a type in parentheses in front of a value is a cast. This program shows the ways it can go right and wrong.
public class Casting {
public static void main(String[] args) {
System.out.println(7 / 2);
System.out.println(7.0 / 2);
System.out.println((double) 7 / 2);
System.out.println((double) (7 / 2));
System.out.println((int) 3.99);
System.out.println((int) -3.99);
double score = 86.7;
System.out.println((int) (score + 0.5));
int a = 5;
int b = 2;
System.out.println((a + b) / 2);
System.out.println((double) (a + b) / 2);
}
}
3
3.5
3.5
3.0
3
-3
87
3
3.5
Line by line, that output is a complete lesson in itself.
7 / 2is 3: two ints, integer division.7.0 / 2is 3.5: one operand is a double, so Java promotes the other and does floating-point division. The type of an arithmetic expression is the wider of the two operand types.(double) 7 / 2is 3.5: the cast binds to the 7 only, giving 7.0 / 2. A cast is tighter than division.(double) (7 / 2)is 3.0: the parentheses forced the integer division to happen first, and the cast then dressed a 3 up as 3.0. This is the single most common mistake with casts, and it is on the exam nearly every year. Cast before you divide, not after.(int) 3.99is 3 and(int) -3.99is minus 3: casting to int truncates towards zero. It never rounds, and for negative numbers truncating is not the same as rounding down.(int) (score + 0.5)is 87: adding a half and then truncating is how you round a positive number to the nearest whole one without a library method. Check it: 86.7 plus 0.5 is 87.2, truncated to 87.
Why this matters: most wrong answers about arithmetic in Java are not arithmetic mistakes. They are type mistakes. Before you predict the value of an expression, decide the type of every operand, because that decides which operation the symbol stands for.
The int that ran out of room
An int uses exactly 32 bits, so there are 2 to the 32nd power, or 4294967296, different values it can hold. Half are used for negative numbers. Run past the top and the count does not stop; it wraps round to the bottom, the way an odometer rolls from 999999 to 000000.
public class Doubling {
public static void main(String[] args) {
int n = 1073741824;
System.out.println("start " + n);
System.out.println("doubled " + n * 2);
System.out.println("max " + Integer.MAX_VALUE);
System.out.println("max plus 1 " + (Integer.MAX_VALUE + 1));
System.out.println("min " + Integer.MIN_VALUE);
}
}
start 1073741824
doubled -2147483648
max 2147483647
max plus 1 -2147483648
min -2147483648
Doubling a billion produced a negative two billion, and adding one to the largest int produced the smallest. This is integer overflow, and the thing that makes it dangerous is the silence: no error, no warning, just a wrong number carried forward into everything that follows. Java's constants Integer.MAX_VALUE and Integer.MIN_VALUE exist so that you can check before you calculate.
Doubles are approximations, and 0.1 is the proof
The line that printed 0.30000000000000004 was not a rounding bug. A double stores a number in binary, and one tenth in binary is a repeating fraction exactly as one third is in decimal. Cut it off after 52 bits and you have stored something very slightly different from 0.1. Add two such values and the small errors do not cancel.
The consequence is a rule with no exceptions: never compare two doubles with the equals sign. Ask instead whether the difference between them is small: Math.abs(x - y) < 0.0001. And when you display money or a measurement, decide on the number of digits when you print rather than trusting the stored value to be exact.
Operators, shorthand, and the order they run in
Java's arithmetic operators are +, -, *, / and %, and they follow the precedence you learned in arithmetic: multiplication, division and remainder before addition and subtraction, left to right within a level, parentheses first. Assignment happens last of all, which is why x = x + 1 works.
That last pattern is so common that Java gives it a shorthand: the compound assignment operators. And adding or subtracting one has an even shorter form.
public class Ops {
public static void main(String[] args) {
int x = 10;
x += 5;
System.out.println(x);
x -= 3;
System.out.println(x);
x *= 2;
System.out.println(x);
x /= 4;
System.out.println(x);
x %= 4;
System.out.println(x);
int i = 7;
i++;
System.out.println(i);
i--;
System.out.println(i);
System.out.println(5 + 3 * 2 - 4 / 2);
System.out.println((5 + 3) * (2 - 4) / 2);
}
}
15
12
24
6
2
8
7
9
-8
Follow x through: 10, then 15, then 12, then 24, then 6 (note 24 / 4 is exact here), then 2 (6 % 4). Then i goes 7 to 8 to 7. The last two lines are precedence: 5 + 6 - 2 is 9, while 8 times minus 2 is minus 16, divided by 2 is minus 8.
One warning about ++. Used as a statement on its own line it is clear and the exam uses it constantly. Buried inside a larger expression, as in arr[i++] = i, it becomes a puzzle rather than code, and free-response answers that do it are hard to read and easy to get wrong. Keep it on its own line.
Common misconceptions
- "Integer division rounds." It truncates. 7 / 2 is 3 and 99 / 100 is 0, and negative values truncate towards zero, so -7 / 2 is -3 rather than -4.
- "Casting to double fixes the division." Only if the cast lands before the division.
(double) (7 / 2)printed 3.0 above, because the damage was already done inside the parentheses. - "Java would tell me if a number got too big." It does not. Overflow is silent, which is precisely why it is worth knowing that an int stops at 2147483647.
- "0.1 + 0.2 == 0.3 is true." It is false. Two doubles that should be equal often differ in the last bits, so compare the size of their difference instead.
- "A declaration and an assignment are the same thing."
int x = 5;does both at once, butint x;declares without assigning, and using x before it has a value is a compile error, not a zero.
Putting it together
Everything in this lesson follows from one sentence: in Java the type of the operands decides what an operator does. The same slash means truncating division between two ints and exact division as soon as one side is a double. The same plus adds two numbers and joins a String to anything. An int has a hard ceiling and wraps round silently when you pass it; a double has no such ceiling but stores most decimal fractions slightly wrong.
The upshot: before predicting the value of a Java expression, write the type above each operand. Do that and integer division, casting and overflow questions stop being tricks and become bookkeeping.
Next lesson you meet a second kind of value entirely. Everything here has been a primitive, a value stored directly in its variable. Strings and other objects are stored by reference, and that one difference explains a piece of behaviour that catches nearly everyone: two Strings that print identically can still fail an equality test.
Sources
- Oracle. (n.d.). The Java Tutorials: Primitive Data Types (the int as a 32-bit signed two's complement integer; the double as 64-bit IEEE 754, with the warning against using it for exact values). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Operators (arithmetic, compound assignment, increment and decrement, and operator precedence). docs.oracle.com
- Oracle. (2023). The Java Language Specification, Java SE 21 Edition, Chapter 4: Types, Values, and Variables. docs.oracle.com
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 2: Variables and Operators. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Primitive type
- A type whose variable holds the value itself. int, double and boolean do nearly all the work in this course.
- Integer division
- Division between two ints, which discards the fractional part rather than rounding: 7 / 2 is 3.
- Remainder operator
- The percent sign, giving what is left after integer division: 7 % 2 is 1.
- Cast
- A type written in parentheses before a value, converting it: (double) 7 or (int) 3.99.
- Integer overflow
- What happens when a result passes 2147483647: the value wraps round to the negative end, with no error.
- Compound assignment
- Shorthand such as x += 5, meaning x = x + 5. The family includes -=, *=, /= and %=.
- Declaration
- A statement naming a variable and fixing its type, optionally giving it a starting value.
- Lossy conversion
- A narrowing that would lose information, such as double to int. Java refuses it unless you write a cast.
Module 2: Using Objects You Did Not Write
Before you build classes of your own you learn to use other people's. This module introduces the reference variable and the object it points at, works through the String methods the exam expects you to know, and settles the question that produces more wrong answers than any other: when two things that look equal are not the same.
References, constructors, and the String class
- Distinguish a primitive variable from a reference variable and say what each one stores.
- Use length, substring, indexOf, equals and compareTo on Strings and predict their results.
- Explain why == can be false for two Strings that print identically.
Two variables, two different pictures
Last lesson every variable held a number or a truth value, and the value sat inside the variable. Here is the other kind. Read the two declarations and notice that they look almost identical.
int count = 12;
String phrase = "Ada Lovelace";
The first is a primitive variable. The bit pattern for 12 is in the box called count. The second is a reference variable. The characters A, d, a and the rest are not in the box called phrase. They are in an object somewhere else in memory, and phrase holds a way of reaching that object, the way a street address is not a house but gets you to one.
| Primitive variable | Reference variable | |
|---|---|---|
| Types | int, double, boolean | String, ArrayList, any class |
| What it holds | The value itself | A reference to an object |
| Copying it | Copies the value | Copies the reference, not the object |
| Testing with == | Compares values | Compares references |
| Can be empty? | No | Yes, the value null |
| Methods | None | Called with a dot |
Every row of that table is going to cost somebody a point on the exam, and the row about == costs the most. Work down it once, then watch each line turn into behaviour in a real program.
Making an object, and calling a method on it
Most objects are made with the keyword new followed by a constructor, a special method with the same name as the class whose job is to set the new object up.
String copy = new String("hello");
String is the exception that proves the rule, because it is common enough to have its own shorthand: writing a piece of text in double quotes creates a String object without any use of new. Nearly all your Strings will be made that way. Every other class you meet in this course, including your own, uses new.
Once you have an object you talk to it through the dot. phrase.length() means: go to the object phrase refers to, and run its length method. Oracle's documentation for the String class lists dozens of methods. This course uses a handful, chosen because they are the ones the exam works from.
| Call | Gives back | On "Ada Lovelace" |
|---|---|---|
s.length() | int, the number of characters | 12 |
s.substring(4) | String, from index 4 to the end | Lovelace |
s.substring(0, 3) | String, index 0 up to but not including 3 | Ada |
s.indexOf("Love") | int, first position, or -1 if absent | 4 |
s.equals(t) | boolean, same characters in the same order | depends on t |
s.compareTo(t) | int, negative, zero or positive | depends on t |
Two habits will save you. First, indexes start at 0, so the first character of a String of length 12 is at index 0 and the last is at index 11. Second, substring(from, to) includes the character at from and excludes the one at to, which means the length of the result is always to minus from. If you want one character at index i, ask for substring(i, i + 1). That idiom is used everywhere in this course.
Here is the table as a running program.
public class Words {
public static void main(String[] args) {
String phrase = "Ada Lovelace";
System.out.println(phrase.length());
System.out.println(phrase.substring(4));
System.out.println(phrase.substring(0, 3));
System.out.println(phrase.indexOf("Love"));
System.out.println(phrase.indexOf("z"));
System.out.println(phrase.substring(4, 5));
System.out.println(phrase + " has " + phrase.length() + " characters");
}
}
12
Lovelace
Ada
4
-1
L
Ada Lovelace has 12 characters
The minus one is worth pausing on. There is no z in the phrase, and rather than failing, indexOf reports minus one, a value no real index could be. You will test for it constantly: if (s.indexOf(target) >= 0) is how you ask whether one piece of text occurs inside another.
A String cannot be changed, only replaced
Oracle's tutorial on strings states the rule in one sentence: the String class is immutable, so that once it is created a String object cannot be changed. Every method that seems to modify a String actually builds a new one and hands it back. This program makes the consequence visible.
public class Immutable {
public static void main(String[] args) {
String s = "cat";
s.substring(1);
System.out.println(s);
s = s + "s";
System.out.println(s);
String t = s;
s = s + "!";
System.out.println("t is " + t);
System.out.println("s is " + s);
}
}
cat
cats
t is cats
s is cats!
The second line of the program called substring and then threw the answer away, so s still printed cat. Beginners write that line constantly, expecting the object to change. It never does: if you want the result, you must store it.
The last part is the one to keep. After String t = s; both names referred to the same object, holding cats. Then s = s + "!" built a brand new object and pointed s at it. It did not reach into the old object and add anything, so t still says cats. Immutability is what makes copying a String reference safe, and later, when you meet arrays, you will see what happens with an object that is not immutable.
The point: assignment to a reference variable moves the arrow, never the object.
The most expensive four characters in the course: ==
Now the row of the table that costs points. For primitives == asks whether two values are the same number. For references it asks whether two arrows point at the same object. Two different objects holding identical characters are not the same object.
public class SameOrEqual {
public static void main(String[] args) {
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println("a == b " + (a == b));
System.out.println("a == c " + (a == c));
System.out.println("a.equals(c) " + a.equals(c));
String part = "hel";
String e = part + "lo";
System.out.println("a == e " + (a == e));
System.out.println("a.equals(e) " + a.equals(e));
}
}
a == b true
a == c false
a.equals(c) true
a == e false
a.equals(e) true
Five lines, and they are why this rule has to be memorised rather than reasoned about case by case. a == b was true because Java keeps one shared copy of each identical literal in a pool, a trick called interning, so both names ended up on the same object. a == c was false because new always builds a fresh object. And a == e was false even though e holds exactly hello, because it was assembled at run time from a variable and so is a new object.
So == on Strings is not simply wrong; it is worse than wrong. It is right often enough to fool you in testing and then false at the moment that matters. equals was true in every case where the characters matched, which is the question you actually wanted answered.
public class Check {
public static void main(String[] args) {
String typed = "yes";
String stored = new String("yes");
System.out.println("wrong test: " + (typed == stored));
System.out.println("right test: " + typed.equals(stored));
System.out.println("compareTo: " + typed.compareTo(stored));
}
}
wrong test: false
right test: true
compareTo: 0
Remember: for Strings and every other object, use equals for content and reserve == for the question of whether two references point at the same object, which is nearly always a question about null.
Ordering text with compareTo
equals answers yes or no. When you need alphabetical order you need compareTo, which returns an int: negative if this String comes first, zero if they are equal, positive if it comes later.
public class Compare {
public static void main(String[] args) {
System.out.println("apple".compareTo("banana"));
System.out.println("banana".compareTo("apple"));
System.out.println("apple".compareTo("apple"));
System.out.println("Apple".compareTo("apple"));
System.out.println("apple".compareTo("applesauce"));
}
}
-1
1
0
-32
-5
Read the numbers. Apple against banana gives minus one, because a comes one place before b. Reversing the two flips the sign. Equal Strings give zero. Capital A against lowercase a gives minus 32, because comparison works on character codes and in ASCII the capitals sit 32 below the lowercase letters, so every capital letter sorts before every lowercase one. And when one String is a prefix of the other, the answer is the difference in length: apple against applesauce gives minus 5.
The exam never asks you to reproduce the exact number, only its sign, and it usually appears inside a condition such as if (name.compareTo(other) < 0). Write your conditions that way and the size of the number never matters.
When the reference points nowhere
A reference variable can hold null, which means it points at no object at all. Calling a method through it fails at run time.
public class NullDemo {
public static void main(String[] args) {
String name = null;
System.out.println("about to ask for its length");
System.out.println(name.length());
}
}
about to ask for its length
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
at NullDemo.main(NullDemo.java:5)
Notice that the first line printed. This is a run-time error, not a compile error: the program began, did some work and then failed. Modern Java tells you which call failed and which variable was null, which is a substantial improvement on the bare message older versions gave.
A second run-time failure worth meeting now comes from asking a String for characters it does not have.
public class Crash {
public static void main(String[] args) {
String word = "java";
System.out.println("before");
System.out.println(word.substring(2, 9));
System.out.println("after");
}
}
The last three lines of the real stack trace:
at java.base/java.lang.String.checkBoundsBeginEnd(String.java:4855)
at java.base/java.lang.String.substring(String.java:2823)
at Crash.main(Crash.java:5)
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [2, 9) out of bounds for length 4
Read a stack trace from the bottom up: the deepest line that names your file is where to look, and here it is Crash.java line 5. The word after and everything following it never ran, because an uncaught exception stops the program where it stands. Lesson 21 is devoted to reading these.
Common misconceptions
- "== compares the text in two Strings." It compares references. The program above printed false for two Strings both holding hello. Use equals for content, every time, with no exceptions.
- "substring changes the String." Nothing changes a String.
s.substring(1);on its own line does real work and then discards it; you must assign the result somewhere. - "substring(2, 9) just stops at the end of a short String." It throws StringIndexOutOfBoundsException, as the trace above shows. The second argument may equal the length but may not exceed it.
- "An empty String and null are the same." An empty String is a real object with length 0 and you may call methods on it. A null reference points at nothing, and calling a method on it fails.
- "indexOf returns 0 when the text is missing." It returns -1. Zero means found at the very start, which is a completely different answer.
What to carry forward
A reference variable holds a way to reach an object, not the object itself, and that single fact explains everything in this lesson: why copying a String variable does not copy the text, why == answers a question you did not ask, why a null reference blows up at run time, and why immutability makes sharing a String harmless. The methods to have at your fingertips are length, substring with one argument and with two, indexOf, equals and compareTo, with indexes starting at zero and the second argument of substring excluded.
Worth holding on to: when an exam question puts two Strings side by side, ask yourself which question is being asked, same object or same characters. The first is ==. The second, and almost always the one you want, is equals.
Next lesson finishes the toolkit of objects you did not write: the Math class, which you never make an object of, and the wrapper classes Integer and Double, which turn a primitive into an object so it can live in an ArrayList later on. The same == trap appears there in a new disguise.
Sources
- Oracle. (n.d.). The Java Tutorials: Strings (the String class is immutable, so once created a String object cannot be changed). docs.oracle.com
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class String (length, substring, indexOf, equals and compareTo, including the value returned when the text is absent). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Creating Objects (the new operator, constructors, and what a reference variable holds). docs.oracle.com
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 9: Immutable Objects. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Reference variable
- A variable holding a way to reach an object rather than the object itself.
- Object
- A value built from a class, living in memory and reached through a reference.
- Constructor
- The method called by new to set a new object up; it has the same name as the class.
- Immutable
- Unable to be changed after creation. String is immutable, so its methods return new Strings.
- null
- The value of a reference that points at no object. Calling a method through it throws NullPointerException.
- substring(from, to)
- The characters from index from up to but not including index to, so its length is to minus from.
- equals
- The method that compares the contents of two objects. For Strings it asks whether the characters match.
- compareTo
- A method returning a negative number, zero or a positive number to place one String before, with or after another.
Math, wrapper classes, and the == trap
- Call the Math methods on the AP reference sheet and say what type each one returns.
- Produce a random int in a chosen range with the standard cast and shift formula.
- Turn text into numbers with Integer.parseInt and Double.parseDouble.
- Explain why == can be false for two Integer objects holding the same number.
Nine blocks east, twelve blocks south
You leave school, walk nine blocks east, then twelve blocks south. How far are you from the front gate in a straight line? The streets are a grid, so this is the right-angled triangle from geometry: the answer is the square root of nine squared plus twelve squared. Java has no squaring operator and no square-root operator. Both jobs belong to methods in the Math class, and that class is the whole reason this lesson exists.
public class Trip {
public static void main(String[] args) {
int eastWest = 9;
int northSouth = -12;
System.out.println("east-west leg: " + Math.abs(eastWest));
System.out.println("north-south leg: " + Math.abs(northSouth));
double straightLine = Math.sqrt(Math.pow(eastWest, 2) + Math.pow(northSouth, 2));
System.out.println("straight line: " + straightLine);
System.out.println("pow(9, 2) alone: " + Math.pow(eastWest, 2));
System.out.println("sqrt of 2: " + Math.sqrt(2));
System.out.println("blocks walked: " + (Math.abs(eastWest) + Math.abs(northSouth)));
}
}
Compiled and run, it prints:
east-west leg: 9
north-south leg: 12
straight line: 15.0
pow(9, 2) alone: 81.0
sqrt of 2: 1.4142135623730951
blocks walked: 21
Four things in that output are worth stopping for.
The straight-line distance is exactly 15, and it printed as 15.0. That decimal point is not decoration. Math.sqrt hands back a double, and a double always prints with a point even when its value happens to be a whole number. The same thing happened to Math.pow(9, 2), which gave 81.0 rather than 81, because pow also returns a double. If an exam question shows you System.out.println(Math.pow(2, 3)); and offers both 8 and 8.0, the answer is 8.0.
The square root of 2 printed seventeen digits, which is what a double does with a number that has no exact decimal form. Nothing was rounded for you.
Math.abs(-12) gave 12 with no decimal point, because there are two abs methods and the one that takes an int returns an int. Java picked that one because northSouth is an int. The last line adds two of those ints and stays an int: 21 blocks of actual walking, as opposed to 15 blocks of straight line.
Finally, look at how every one of those calls is written. Nowhere did the program say new Math(). You did not make a Math object, because there is no such thing worth making. The class name itself sits before the dot.
A static method belongs to the class, not to an object
Last lesson you called phrase.length(), where phrase was a variable holding an object. That is an instance method: it does something to one particular object. Math.sqrt(2) is a static method, also called a class method. It needs no object at all, because a square root does not belong to anything; it is a calculation on the number you hand over. Oracle's tutorial on class variables and class methods puts it in terms of where the member lives: a static member belongs to the class itself, so every use of it goes through the class name.
You will write static methods of your own in Module 4, and the word static in public static void main has been sitting in front of you since Lesson 1 for exactly this reason: main runs before any object exists, so it cannot need one.
Key idea: if a call starts with a capital letter and a dot, it is almost certainly static.
The five lines about Math on the sheet you are given in the exam room
On exam day you get a printed reference booklet called the Java Quick Reference. The College Board describes it as the list of accessible methods from the Java library that may be included on the exam, which is a promise in both directions: nothing on the sheet is off limits, and nothing off the sheet will be assumed. Its Math section has exactly five entries.
| On the sheet | What it gives back | Example, with its real output |
|---|---|---|
static int abs(int x) | int, the size of x without its sign | Math.abs(-12) gives 12 |
static double abs(double x) | double, same idea | Math.abs(-3.5) gives 3.5 |
static double pow(double b, double e) | double, b raised to the power e | Math.pow(9, 2) gives 81.0 |
static double sqrt(double x) | double, the non-negative square root | Math.sqrt(2) gives 1.4142135623730951 |
static double random() | double, at least 0.0 and below 1.0 | a different value every call |
Read the list for what is missing. There is no Math.max, no Math.min, no Math.round, no Math.floor. Real Java has all four, and if you write Math.max(a, b) in a text editor it will compile and work. On this exam you should not rely on it, because a question is allowed to assume you know only the sheet. So when you need the larger of two values you write the comparison yourself, in three lines that are worth having as a reflex.
public class Bigger {
public static void main(String[] args) {
int first = 47;
int second = 53;
int larger = first;
if (second > larger) {
larger = second;
}
System.out.println("larger of 47 and 53 is " + larger);
double a = 3.5;
double b = 3.5;
double smaller = a;
if (b < smaller) {
smaller = b;
}
System.out.println("smaller of 3.5 and 3.5 is " + smaller);
int gap = Math.abs(first - second);
System.out.println("gap between them is " + gap);
}
}
larger of 47 and 53 is 53
smaller of 3.5 and 3.5 is 3.5
gap between them is 6
The pattern is: assume the first one wins, then let the second one take over if it deserves to. It reads oddly the first time and then never bothers you again. Notice also the last line: the distance between two numbers is Math.abs(first - second), which saves you from caring which one is larger. That idiom appears constantly, and it is on the sheet.
A double will not fit in an int, and javac says so
Because pow and sqrt return doubles, this perfectly reasonable-looking line does not compile.
public class Lossy2 {
public static void main(String[] args) {
int side = Math.sqrt(16);
System.out.println(side);
}
}
What javac actually printed:
Lossy2.java:3: error: incompatible types: possible lossy conversion from double to int
int side = Math.sqrt(16);
^
1 error
error: compilation failed
The square root of 16 really is 4, but the compiler does not evaluate your arithmetic to find that out. It reasons about types only: sqrt returns a double, an int cannot hold a double, refuse. The fix is either to store the answer in a double, or to cast on purpose with int side = (int) Math.sqrt(16);, which says you accept the truncation. Casting a square root is how you find whether a number is a perfect square, and Lesson 2 warned you what truncation does to anything that is not exact.
Randomness, and the one formula to memorise
Math.random() returns a double that is at least 0.0 and always less than 1.0. That is nearly useless on its own, because what you want is a die roll or a card or a row number. Getting there takes a multiplication and a cast, in that order.
public class Rolls {
public static void main(String[] args) {
System.out.println("raw: " + Math.random());
System.out.println("raw: " + Math.random());
for (int i = 0; i < 8; i++) {
int die = (int) (Math.random() * 6) + 1;
System.out.print(die + " ");
}
System.out.println();
int low = 10;
int high = 20;
int pick = (int) (Math.random() * (high - low + 1)) + low;
System.out.println("a number from 10 to 20: " + pick);
}
}
One real run of it:
raw: 0.4993107227190906
raw: 0.8615555519838725
2 3 3 3 5 2 5 4
a number from 10 to 20: 13
Your run will print different numbers, and so will the next run of mine. That is the point of the method, and it is also why a random program is awkward to test: you cannot check it by comparing output to an expected answer. What you can check is the range.
Follow the die line one step at a time. Math.random() gives something in the interval from 0.0 up to but not including 1.0. Multiplying by 6 gives something from 0.0 up to but not including 6.0. The cast to int truncates, which leaves one of the whole numbers 0, 1, 2, 3, 4, 5, and exactly six outcomes are possible because 6.0 itself is never reached. Adding 1 shifts them to 1 through 6. The general shape is worth writing on the inside of your eyelids:
(int) (Math.random() * (high - low + 1)) + low
With low 10 and high 20 the multiplier is 11, giving 0 through 10, and the shift lands it on 10 through 20 with both ends included. Two mistakes are extremely common. Forgetting the + 1 inside the parentheses quietly drops the top value, so a program meant to pick 1 to 6 picks 1 to 5. Casting too early, as in (int) Math.random() * 6, truncates the fraction to 0 before the multiplication and prints 0 every single time.
Integer and Double: a primitive wearing an object's coat
An int is not an object. It has no methods, it cannot be null, and, as you will see in Module 5, it cannot go into an ArrayList. Java's answer is a wrapper class: Integer holds one int inside an object, and Double does the same for a double. Each also carries useful static members, and three of those are on the reference sheet: Integer.MIN_VALUE, Integer.MAX_VALUE, and Integer.parseInt, plus Double.parseDouble.
public class Boxes {
public static void main(String[] args) {
int plain = 42;
Integer boxed = 42;
System.out.println("plain " + plain + ", boxed " + boxed);
int unboxed = boxed + 1;
System.out.println("unboxed + 1 = " + unboxed);
String typed = "1947";
int year = Integer.parseInt(typed);
System.out.println("year + 1 = " + (year + 1));
String price = "4.75";
double cost = Double.parseDouble(price);
System.out.println("three of them cost " + (3 * cost));
System.out.println("largest int: " + Integer.MAX_VALUE);
System.out.println("smallest int: " + Integer.MIN_VALUE);
System.out.println("text joined: " + (typed + 1));
}
}
plain 42, boxed 42
unboxed + 1 = 43
year + 1 = 1948
three of them cost 14.25
largest int: 2147483647
smallest int: -2147483648
text joined: 19471
Line 4 of the program is the interesting one. Integer boxed = 42; assigns an int to an object variable, which should be a type error, and the next line adds 1 to that object, which should be worse. Both work because of autoboxing and unboxing: the compiler inserts the conversion for you in either direction. You can mostly forget the machinery, with one exception that gets its own section below.
Compare the two last lines of output. Integer.parseInt("1947") produced the number 1947, so adding 1 gave 1948. But typed + 1, with typed still holding the text, printed 19471, because a plus with a String on one side joins rather than adds. Text that looks like a number is not a number. Every time a program reads a file, a form, or a command line argument, it receives text, and parseInt or parseDouble is the door between the two worlds. If the text is not a number at all, parseInt does not return zero or minus one; it throws NumberFormatException and the program stops.
One digit larger, and the answer flips
Now the trap in the lesson title. Last lesson you learned that == on two String objects compares references, not characters. Integer is an object too, so the same rule applies, and it produces one of the strangest results in Java.
public class BoxTrap {
public static void main(String[] args) {
Integer small1 = 127;
Integer small2 = 127;
Integer big1 = 128;
Integer big2 = 128;
System.out.println("127 == 127 gives " + (small1 == small2));
System.out.println("128 == 128 gives " + (big1 == big2));
System.out.println("equals on 128 gives " + big1.equals(big2));
System.out.println("unboxed compare gives " + (big1.intValue() == big2.intValue()));
}
}
127 == 127 gives true
128 == 128 gives false
equals on 128 gives true
unboxed compare gives true
Nothing changed between the two tests except the number. Java keeps a cache of Integer objects for the small values, so autoboxing 127 twice hands back the same object and == says true. Autoboxing 128 builds two objects, so == says false. A program that tested Integer values with == would pass every test you wrote with small numbers and fail the first time a real value went past 127.
The exam will not ask you where the cache ends, and you should not learn the boundary as a fact. Learn the rule that makes the boundary irrelevant: == on any two object references asks whether they are the same object. For numbers inside wrappers, compare with equals, or take the primitive out and compare that. This is the same lesson as the String one, wearing different clothes, which is why it is worth meeting twice.
The upshot: == is for primitives and for null. For anything with a capital letter at the front of its type, reach for equals.
Common misconceptions
- "Math.pow(2, 3) prints 8." It prints 8.0. Both pow and sqrt return doubles whatever you feed them, and a double always shows a decimal point.
- "You need a Math object before you can use Math." You never make one. Math's methods are static, so the class name goes before the dot, and
new Math()is not something anyone writes. - "(int) Math.random() * 6 gives a number from 0 to 5." It gives 0, always. The cast runs before the multiplication, turning the fraction into 0 first. The parentheses in
(int) (Math.random() * 6)are doing real work. - "Integer and int are interchangeable." They convert into each other automatically, which is not the same thing. Testing two Integer objects with
==can be false for equal values, as the program above showed for 128. - "Integer.parseInt("12.5") gives 12." It throws NumberFormatException. parseInt accepts text that spells an integer and nothing else; for a decimal you need Double.parseDouble.
Where this leaves us
Math holds five entries on the exam reference sheet: abs for ints, abs for doubles, pow, sqrt, and random. All of them are static, so you call them through the class name, and all except the int version of abs return a double, which is where most wrong answers come from. A random int in a range is (int) (Math.random() * (high - low + 1)) + low, with the cast outside the multiplication and the + 1 inside the parentheses. Integer and Double wrap a primitive in an object, convert automatically in both directions, and give you parseInt and parseDouble for turning text into numbers along with Integer.MAX_VALUE and Integer.MIN_VALUE for the limits of an int.
Worth holding on to: you now know two classes you did not write, String and Math, and two wrapper classes. That is the whole of Module 2, and it was deliberately a module about reading other people's work before writing your own.
Module 3 changes the subject from values to control. So far every program has run straight down the page from the first line to the last. Next lesson a program starts making decisions, and the decisions are built out of a type you have barely used: boolean.
Sources
- College Board. (2025). AP Computer Science A: 2026 Exam Reference Information, Java Quick Reference (Math class: abs for int and double, pow, sqrt, random; Integer class: MIN_VALUE, MAX_VALUE, parseInt; Double class: parseDouble). apcentral.collegeboard.org
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class Math (return types of abs, pow, sqrt, and the stated range of random). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Autoboxing and Unboxing (the automatic conversion between int and Integer, and where the compiler inserts it). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Understanding Class Members (class methods belong to the class, so they are called through the class name). docs.oracle.com
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 2: Variables and Operators, and Chapter 3: Input and Output. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Math class
- A library class of static methods for arithmetic. The exam sheet lists abs, pow, sqrt and random.
- Static method
- A method belonging to the class rather than to an object, called through the class name.
- Instance method
- A method called on one particular object through a reference variable and a dot.
- Math.random()
- Returns a double that is at least 0.0 and always less than 1.0.
- Wrapper class
- A class holding one primitive inside an object. Integer wraps an int, Double wraps a double.
- Autoboxing
- The compiler's automatic conversion between a primitive and its wrapper, in either direction.
- Integer.parseInt
- A static method turning text that spells a whole number into an int; it throws NumberFormatException otherwise.
- Integer.MAX_VALUE
- The largest value an int can hold, 2147483647. Integer.MIN_VALUE is the smallest, -2147483648.
Module 3: Boolean Logic and Iteration
Three lessons that give a program the power to choose and to repeat. You start from a grading program that awards a D for a score of 84 and find out why, build compound conditions with && and || and check De Morgan's laws against real output, then meet while and for loops, the off-by-one error that ends every beginner's first loop in an exception, and the nested loops that walk a grid or a string one character at a time.
Boolean expressions, if, else, and De Morgan's laws
- Write an if, else if and else chain that puts its conditions in an order that works.
- Build compound conditions with &&, || and !, and predict their values from a truth table.
- Apply De Morgan's laws to rewrite a negated condition without changing its meaning.
- Explain how short-circuit evaluation prevents a division by zero or a null reference.
A score of 84 that came out as a D
Here is a program that turns a percentage into a letter grade using the boundaries your school probably uses: 90 and above is an A, 80 and above a B, 70 a C, 60 a D, below that an F. It compiles without a warning. It runs without an exception. It is also wrong, and it is wrong in a way that no compiler on earth will tell you about.
public class Grades {
public static void main(String[] args) {
int score = 84;
String grade;
if (score >= 60) {
grade = "D";
} else if (score >= 70) {
grade = "C";
} else if (score >= 80) {
grade = "B";
} else if (score >= 90) {
grade = "A";
} else {
grade = "F";
}
System.out.println("score " + score + " earns grade " + grade);
}
}
Its real output:
score 84 earns grade D
Eighty-four is a B in every school in the country, and the program said D. Trace it. Java tests score >= 60 first. Eighty-four is greater than sixty, so that test is true, so grade = "D" runs, and then Java jumps straight past every remaining else if to the closing brace. The later tests, the ones that would have said B, were never even looked at.
That is the whole behaviour of an if, else if, else chain: the conditions are tried from the top, the first one that is true has its block run, and the rest of the chain is skipped. At most one block ever runs. The chain is not a list of independent questions; it is a single decision with several exits.
So the fix is not in any one condition. Each of the four is correct in isolation. The fix is the order.
Testing the narrowest condition first
public class Grades2 {
public static void main(String[] args) {
int score = 84;
String grade;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else if (score >= 70) {
grade = "C";
} else if (score >= 60) {
grade = "D";
} else {
grade = "F";
}
System.out.println("score " + score + " earns grade " + grade);
int second = 90;
String other;
if (second >= 90) {
other = "A";
} else if (second >= 80) {
other = "B";
} else {
other = "lower";
}
System.out.println("score " + second + " earns grade " + other);
}
}
score 84 earns grade B
score 90 earns grade A
Same five boundaries, reversed order, correct answers. When conditions overlap, put the hardest one to satisfy at the top, because reaching a later branch then carries the information that every earlier branch failed. By the time Java reaches score >= 80 in the fixed version, it already knows the score is below 90, so that branch really does mean "between 80 and 89" even though nothing in the code says so. This is why an else if chain is shorter than a set of separate ifs: the exclusions are free.
The second half of the program checks the boundary itself. A score of exactly 90 gives an A, because >= includes the value on the line. Had the condition been score > 90, a score of 90 would have fallen through to B, and that single character is one of the commonest mistakes on the whole exam. Oracle's tutorial on the if-then and if-then-else statements uses this same grading example, which tells you something about how central it is.
The point: when branches overlap, order is part of the logic, and the compiler cannot check it for you.
What a boolean actually is
Everything above rests on a type you have barely used. A boolean variable holds one of exactly two values, true or false, and those are keywords, not the words true and false in quotation marks. The condition in an if statement must be a boolean. It cannot be a number, and it cannot be text.
Six operators produce booleans out of numbers. They are called the relational operators, and two of them look almost the same as something else.
| Operator | True when | With a = 84 and b = 90 |
|---|---|---|
< | the left is smaller | a < b is true |
<= | smaller or the same | b <= 90 is true |
> | the left is larger | a > b is false |
>= | larger or the same | a >= 84 is true |
== | the two are equal | a == 84 is true |
!= | the two differ | a != b is true |
Three more operators combine booleans into bigger booleans. && is and: true only when both sides are true. || is or: true when at least one side is true. ! is not: it flips a single boolean. An expression built from these is called a compound boolean expression, and the way to be certain of one is a truth table. Here is one Java printed for itself.
public class Truth {
public static void main(String[] args) {
System.out.println("a b a && b a || b !a");
System.out.println("true true " + (true && true) + " " + (true || true) + " " + (!true));
System.out.println("true false " + (true && false) + " " + (true || false) + " " + (!true));
System.out.println("false true " + (false && true) + " " + (false || true) + " " + (!false));
System.out.println("false false " + (false && false) + " " + (false || false) + " " + (!false));
int age = 15;
boolean canVote = age >= 18;
boolean isTeen = age >= 13 && age <= 19;
System.out.println("age " + age + ": canVote " + canVote + ", isTeen " + isTeen);
System.out.println("not a teenager: " + !isTeen);
System.out.println("0.1 + 0.2 == 0.3 gives " + (0.1 + 0.2 == 0.3));
System.out.println("the sum itself is " + (0.1 + 0.2));
}
}
a b a && b a || b !a
true true true true false
true false false true false
false true false true true
false false false false true
age 15: canVote false, isTeen true
not a teenager: false
0.1 + 0.2 == 0.3 gives false
the sum itself is 0.30000000000000004
The four rows are the whole of and and or, and the exam expects you to know them without deriving them. Look at the second block too. A relational expression can be stored: boolean canVote = age >= 18; saves the answer rather than the question, and isTeen shows the standard way to test a range, which is two comparisons joined with and. There is no 13 <= age <= 19 in Java. Writing that does not compile.
The last two lines are a warning from Lesson 2 coming back as a boolean. One tenth plus two tenths is not exactly three tenths in binary, so == on doubles answers false for a sum that any human would call equal. Never test two computed doubles for equality. Test whether the gap between them is small: Math.abs(x - y) < 0.0001.
The single equals sign, and why javac catches it in Java
In some languages, writing if (score = 90) when you meant == is a silent disaster: the assignment happens, the value is treated as true, and the bug hides for years. Java's insistence that a condition be a boolean turns that into a compile error.
public class Assign {
public static void main(String[] args) {
int score = 84;
if (score = 90) {
System.out.println("top marks");
}
}
}
What javac really printed:
Assign.java:4: error: incompatible types: int cannot be converted to boolean
if (score = 90) {
^
1 error
error: compilation failed
Read the caret. It is under the equals sign, which is exactly where the mistake is. score = 90 is an assignment, and an assignment in Java produces the value assigned, an int, and an int is not a boolean. Oracle's page on the equality, relational and conditional operators states the rule directly: use ==, not =, when testing whether two primitive values are equal. One place this protection does not reach is a boolean variable, where if (finished = true) compiles happily and assigns. Write if (finished) and the problem cannot arise.
De Morgan's laws, checked line by line
Half the conditions you will read on an exam paper have a ! wrapped round them, and you need to move it inside without changing the meaning. De Morgan's laws, named for the nineteenth-century logician Augustus De Morgan, say how. In words: the negation of an and is the or of the negations, and the negation of an or is the and of the negations. In Java:
!(a && b) is the same as !a || !b
!(a || b) is the same as !a && !b
Notice that the operator itself changes. Distributing the not over an and gives you an or. Students who remember to flip each side but forget to flip the connective get exactly half the questions right, which is the worst possible outcome because it feels like understanding. Here is Java checking both laws in all four cases.
public class Morgan {
public static void main(String[] args) {
System.out.println("a b !(a&&b) !a||!b !(a||b) !a&&!b");
System.out.println("true true " + !(true && true) + " " + (!true || !true)
+ " " + !(true || true) + " " + (!true && !true));
System.out.println("true false " + !(true && false) + " " + (!true || !false)
+ " " + !(true || false) + " " + (!true && !false));
System.out.println("false true " + !(false && true) + " " + (!false || !true)
+ " " + !(false || true) + " " + (!false && !true));
System.out.println("false false " + !(false && false) + " " + (!false || !false)
+ " " + !(false || false) + " " + (!false && !false));
int age = 70;
boolean member = false;
System.out.println("not (young or old): " + !(age < 5 || age > 65));
System.out.println("old enough and not old: " + (age >= 5 && age <= 65));
System.out.println("member is not true: " + !member);
}
}
a b !(a&&b) !a||!b !(a||b) !a&&!b
true true false false false false
true false true true false false
false true true true false false
false false true true true true
not (young or old): false
old enough and not old: false
member is not true: true
Column two matches column three in every row, and column four matches column five in every row. That is the proof, done by exhaustion over the only four cases there are, which is a technique worth keeping: a boolean expression in two variables has four possible inputs, so you can always just check them all.
The last three lines apply it to something real. A swimming pool lets under-fives and over-sixty-fives in free. A swimmer of 70 is over 65, so "not (under five or over sixty-five)" is false, and De Morgan's rewrite, "at least five and at most sixty-five", is false as well. Negating the relational operators is a second step people forget: the opposite of age < 5 is age >= 5, not age > 5.
Short-circuit evaluation is not a speed trick, it is a guard
When Java evaluates a && b and finds a false, the answer is already settled, so it never evaluates b at all. When it evaluates a || b and finds a true, likewise. Oracle's tutorial calls this short-circuiting and says the second operand is evaluated only if needed. That is not a footnote about performance. It is the only thing standing between many programs and a crash.
public class Guard {
public static void main(String[] args) {
int total = 50;
int count = 0;
if (count != 0 && total / count > 10) {
System.out.println("the average is above 10");
} else {
System.out.println("no safe average to report");
}
String name = null;
if (name != null && name.length() > 3) {
System.out.println("a long name");
} else {
System.out.println("no name, and no crash");
}
System.out.println("about to swap the two tests round");
if (total / count > 10 && count != 0) {
System.out.println("unreachable");
}
}
}
The real output, ending in a real failure:
no safe average to report
no name, and no crash
about to swap the two tests round
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Guard.main(Guard.java:17)
The first two conditions each put a test in front of a dangerous operation. count != 0 is false, so total / count was never worked out. name != null is false, so name.length() was never called, and the NullPointerException from Lesson 3 never happened. Then line 17 writes the same two tests in the other order, and the division runs first, because nothing has told Java to skip it yet. ArithmeticException, and the program stops.
So && is not commutative in practice, even though the truth table says the value is the same either way. Put the cheap, safe test on the left and let it protect what follows. That pattern, a null check or a zero check followed by &&, is worth recognising instantly, because it appears in nearly every real Java program you will ever read.
The semicolon that switches an if off
One last trap, and it is the reason experienced programmers use braces even for a single statement.
public class Oops {
public static void main(String[] args) {
int age = 10;
if (age >= 18);
System.out.println("allowed in");
System.out.println("age was " + age);
int other = 10;
if (other >= 18)
System.out.println("first line, guarded");
System.out.println("second line, not guarded");
}
}
allowed in
age was 10
second line, not guarded
A ten-year-old was allowed in. The semicolon straight after if (age >= 18) is a complete, empty statement, so that is the whole body of the if: when the condition is true, Java does nothing. The println after it is simply the next statement in the method, run unconditionally. It compiles because nothing about it is illegal.
The second half shows the indentation trap. Without braces an if governs exactly one statement, so the first println was guarded and skipped, while the second was not part of the if at all and printed even though the condition was false. Indentation is invisible to the compiler. Braces are not. Write them every time and neither of these can happen to you.
Common misconceptions
- "Every branch of an if, else if chain whose condition is true will run." Only the first one does. Grades.java assigned D to a score of 84 and never looked at the B test.
- "You can write 13 <= age <= 19." That does not compile in Java. A range needs two comparisons joined with
&&. - "!(a && b) is !a && !b." The connective flips too. It is
!a || !b, and the truth table above shows the two disagree in three of four rows. - "Short-circuiting is just an optimisation, so the order of my tests does not matter." Swapping the order in Guard.java turned a clean run into an ArithmeticException. The guard has to come first.
- "if (finished = true) is the same as if (finished)." The first assigns true to finished and then tests it, so it is always true. With an int the compiler rejects it; with a boolean it does not, so this one really can reach a running program.
- "Two doubles that look equal are equal." Truth.java printed false for 0.1 + 0.2 == 0.3. Compare the size of the difference instead.
What to remember
A boolean holds true or false and nothing else, and it is the only thing an if condition may be. The relational operators make booleans out of numbers, and &&, || and ! make bigger booleans out of smaller ones, with the four-row truth table settling any argument. An if, else if, else chain runs at most one block, the first whose condition is true, so overlapping conditions must be ordered from narrowest to widest. De Morgan's laws move a ! inside a bracket, flipping the connective as well as both sides, and each relational operator has an opposite that includes the boundary the original excluded. Short-circuit evaluation lets a left-hand test protect a right-hand operation that would otherwise divide by zero or dereference null.
In short: when a condition confuses you, write out the four rows. It takes thirty seconds and it is never wrong.
Next lesson the program stops running straight through once and starts going round. The same boolean you just learned becomes the thing that decides whether a loop takes another turn, and the commonest bug in programming, off by one, gets caught in the act.
Sources
- Oracle. (n.d.). The Java Tutorials: The if-then and if-then-else Statements (control jumps past the rest of the chain once a branch is taken; the grade-boundary example). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Equality, Relational, and Conditional Operators (the short-circuiting behaviour of && and ||, and the warning to use == rather than = when testing equality). docs.oracle.com
- Oracle. (2023). The Java Language Specification, Java SE 21, section 15.23: Conditional-And Operator (the right operand is evaluated only when the left is true). docs.oracle.com
- Wikipedia contributors. (2026). De Morgan's laws (the negation of a conjunction is the disjunction of the negations, and conversely). en.wikipedia.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 5: Conditionals and Logic. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- boolean
- A primitive type holding exactly one of two values, the keywords true and false.
- Relational operator
- One of <, <=, >, >=, == and !=, each producing a boolean from two values.
- Compound boolean expression
- A condition built from smaller ones with &&, || or !.
- if, else if, else chain
- A single decision with several exits. At most one block runs: the first whose condition is true.
- Short-circuit evaluation
- Skipping the right operand of && when the left is false, or of || when the left is true.
- De Morgan's laws
- !(a && b) equals !a || !b, and !(a || b) equals !a && !b. The connective flips as well as the sides.
- Equivalent boolean expressions
- Two conditions that give the same value for every possible input, which four rows of a truth table can settle.
while and for: counting without going one too far
- Write a while loop and a for loop that perform the same repetition, and convert between them.
- Count the number of trips a loop makes and pick bounds that produce the count you meant.
- Recognise an off-by-one error and an infinite loop from the symptoms each one produces.
- Use the accumulator pattern to build a total, a product or a count inside a loop.
Five lines of countdown, typed out and then not typed out
A launch countdown prints 5, 4, 3, 2, 1 and then the words lift off. You could write six printlns. Now make it a countdown from a hundred, and you cannot. Repetition is the reason loops exist, and Java has two you need, both of which do exactly the same thing in slightly different clothes.
public class Countdown {
public static void main(String[] args) {
int seconds = 5;
while (seconds > 0) {
System.out.println(seconds + "...");
seconds = seconds - 1;
}
System.out.println("lift off");
System.out.println("seconds ended at " + seconds);
}
}
5...
4...
3...
2...
1...
lift off
seconds ended at 0
A while loop has two parts: a condition in brackets and a body in braces. Java tests the condition. If it is true, the body runs, and then Java goes back and tests the condition again. If it is false, the loop is finished and execution carries on below. That is the whole mechanism, and it means a while loop needs three things from you or it will not behave: something set up before the loop, a condition that can become false, and a change inside the body that moves you towards it.
Miss any one of the three and you have a bug with a name. No setup and the code does not compile. No change inside the body and the loop runs forever. A condition that the change can never falsify, and the same. That last one gets its own section further down, because it is more subtle than it sounds.
The final line of output is worth noticing. The loop stopped when seconds hit 0, which means 0 was never printed, and the variable is still sitting there at 0 afterwards. A loop variable does not disappear when the loop ends; it holds the first value that failed the test.
while and for, side by side
A huge proportion of loops follow one shape: set a counter, test it, change it by a fixed step. The for loop exists to put those three pieces on one line where you can see them together.
| while | for | |
|---|---|---|
| Where the setup goes | on a line above the loop | first of the three parts in the brackets |
| Where the condition goes | in the brackets | second of the three parts |
| Where the change goes | anywhere in the body | third of the three parts |
| Scope of the counter | outlives the loop | ends with the loop when declared inside |
| Best for | repeating until a condition changes | counting a known number of trips |
| Risk | forgetting the change, so it never ends | getting the bounds one out |
Read that table, then read the same five loops Java ran for me.
public class Same {
public static void main(String[] args) {
System.out.println("with while:");
int i = 1;
while (i <= 5) {
System.out.print(i + " ");
i++;
}
System.out.println();
System.out.println("i afterwards is " + i);
System.out.println("with for:");
for (int j = 1; j <= 5; j++) {
System.out.print(j + " ");
}
System.out.println();
System.out.println("counting from 0, which is what arrays want:");
for (int k = 0; k < 5; k++) {
System.out.print(k + " ");
}
System.out.println();
System.out.println("stepping by 2:");
for (int m = 0; m <= 10; m += 2) {
System.out.print(m + " ");
}
System.out.println();
System.out.println("downwards:");
for (int n = 5; n >= 1; n--) {
System.out.print(n + " ");
}
System.out.println();
}
}
with while:
1 2 3 4 5
i afterwards is 6
with for:
1 2 3 4 5
counting from 0, which is what arrays want:
0 1 2 3 4
stepping by 2:
0 2 4 6 8 10
downwards:
5 4 3 2 1
The first two loops print identical output, which is the point: for (int j = 1; j <= 5; j++) is the while loop above it with the three pieces gathered up. Then the variations. Counting from 0 while the test says < 5 gives 0, 1, 2, 3, 4, and that is the form every array loop in this course will take, so get used to the look of it now. The step does not have to be 1: m += 2 gives evens. And a loop can run downwards with n-- and a >= test, which you will need when you reverse an array and when you remove things from a list.
One difference is easy to miss. The while loop's i was declared above the loop, so it still existed afterwards and printed as 6. The for loops declared their counters inside the brackets, which means those names stop existing at the closing brace. Trying to print j after the loop is a compile error, not a wrong value. That is usually what you want, and it is why for is the safer of the two whenever it fits.
When the update runs, and the loop that never runs at all
Two questions decide most exam tracing problems. Where in the cycle does the update happen, and can a loop run zero times?
public class Order {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
System.out.println("body sees i = " + i);
}
System.out.println("---");
int i = 1;
while (i <= 3) {
System.out.println("before update, i = " + i);
i++;
System.out.println("after update, i = " + i);
}
System.out.println("loop ended with i = " + i);
System.out.println("---");
int trips = 0;
for (int j = 10; j < 5; j++) {
trips++;
}
System.out.println("a loop whose condition starts false ran " + trips + " times");
}
}
body sees i = 1
body sees i = 2
body sees i = 3
---
before update, i = 1
after update, i = 2
before update, i = 2
after update, i = 3
before update, i = 3
after update, i = 4
loop ended with i = 4
---
a loop whose condition starts false ran 0 times
The body never saw 4. The order of events in a for loop is: run the initialisation once, test the condition, run the body, run the update, test the condition, run the body, and so on. The update happens after the body, not before, so the body always sees the value the condition just approved. The loop ends with the variable holding the first value that failed the test, which is 4 for both loops, and that value is visible afterwards only in the while version.
The last loop ran zero times. Its condition was false on the very first test, so the body never ran once. This matters more than it looks: a loop that runs zero times is normal, expected, and often correct, which is why a program that sums an empty collection should report 0 rather than crash. Java's while and for both test before the body, so both can run zero times. Only do and while, which this course does not use and the exam does not require, always runs at least once.
Remember: condition first, body second, update third, then round again.
Counting the trips: the fence-post rule
Ask a builder how many posts a thirty-metre fence needs with a post every three metres and they will say eleven, not ten, because there is a post at both ends. Loop bounds have exactly this problem, and the only cure is to count the trips deliberately instead of guessing.
public class Fence {
public static void main(String[] args) {
int posts = 0;
for (int i = 0; i < 5; i++) {
posts++;
}
System.out.println("i < 5 gave " + posts + " trips");
posts = 0;
for (int i = 0; i <= 5; i++) {
posts++;
}
System.out.println("i <= 5 gave " + posts + " trips");
posts = 0;
for (int i = 1; i <= 5; i++) {
posts++;
}
System.out.println("i = 1 to i <= 5 gave " + posts + " trips");
posts = 0;
for (int i = 3; i < 9; i++) {
posts++;
}
System.out.println("i = 3 to i < 9 gave " + posts + " trips");
}
}
i < 5 gave 5 trips
i <= 5 gave 6 trips
i = 1 to i <= 5 gave 5 trips
i = 3 to i < 9 gave 6 trips
Two rules come out of those four numbers, and they are worth more to you than any amount of loop theory. When a loop starts at a value and tests with <, the number of trips is the upper value minus the starting value: 5 minus 0 is 5, and 9 minus 3 is 6. When it tests with <=, add one: 5 minus 0 plus 1 is 6. The exam asks how many times a loop body executes constantly, and this arithmetic answers it without tracing a single line.
It also explains why array code always starts at 0 and tests with <. An array of five things has indexes 0 to 4, and for (int i = 0; i < 5; i++) visits each exactly once. Switch that < to <= and you visit index 5, which does not exist.
The off-by-one error caught in the act
Here is that mistake on a String instead of an array, which lets us see it fail for real. The word is four letters long, so its valid indexes are 0, 1, 2 and 3. The loop condition is i <= word.length().
public class OffByOne {
public static void main(String[] args) {
String word = "loop";
System.out.println("length is " + word.length());
for (int i = 0; i <= word.length(); i++) {
System.out.println("index " + i + " holds " + word.substring(i, i + 1));
}
}
}
What actually happened, with the last lines of the real stack trace:
length is 4
index 0 holds l
index 1 holds o
index 2 holds o
index 3 holds p
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Range [4, 5) out of bounds for length 4
at java.base/java.lang.String.checkBoundsBeginEnd(String.java:4855)
at java.base/java.lang.String.substring(String.java:2823)
at OffByOne.main(OffByOne.java:6)
Four correct lines, then a crash. That is the signature of an off-by-one error, and it is why the failure is so recognisable once you have seen it: the program works, visibly, right up to the last element, and then dies on the trip that should not have existed. Change the <= to < and it is correct.
Read the trace from the bottom. The deepest line naming your own file is at OffByOne.main(OffByOne.java:6), so line 6 is where to look, and the message tells you the range asked for was 4 up to 5 in something of length 4. Java's error messages for this are unusually helpful. Use them.
The condition that never becomes false
A loop can have all three of its pieces and still never stop, if the change can step straight over the value the condition is watching for.
public class Runaway {
public static void main(String[] args) {
int i = 1;
int safety = 0;
while (i != 10 && safety < 8) {
System.out.print(i + " ");
i = i + 2;
safety++;
}
System.out.println();
System.out.println("stopped because safety reached " + safety + ", with i at " + i);
int j = 1;
while (j < 10) {
j = j + 2;
}
System.out.println("the same loop written with < ended cleanly, j = " + j);
}
}
1 3 5 7 9 11 13 15
stopped because safety reached 8, with i at 17
the same loop written with < ended cleanly, j = 11
Starting at 1 and adding 2 produces only odd numbers, so i != 10 is true forever. The safety counter is scaffolding I added so the program would finish and you could read its output; without it, this loop prints odd numbers until you kill the process. The second loop fixes it by asking whether j has passed 10 rather than whether it has landed on it, and it ends at 11.
The general lesson: write loop conditions with <, <=, > or >= rather than != or == whenever the counter moves by more than one. An inequality cannot be jumped over. An equality can.
The accumulator, the one pattern you will use most
Almost every useful loop builds up an answer as it goes. The pattern has a name, the accumulator, and it has three parts: declare the variable before the loop, update it inside, use it after.
public class Accumulate {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println("1 through 100 adds up to " + sum);
int product = 1;
for (int i = 1; i <= 6; i++) {
product *= i;
}
System.out.println("6 factorial is " + product);
int value = 1;
int doublings = 0;
while (value < 1000) {
value *= 2;
doublings++;
}
System.out.println("doubling from 1 passed 1000 after " + doublings + " steps, at " + value);
int countdownSum = 0;
for (int i = 100; i >= 1; i--) {
countdownSum += i;
}
System.out.println("the same total counting down: " + countdownSum);
}
}
1 through 100 adds up to 5050
6 factorial is 720
doubling from 1 passed 1000 after 10 steps, at 1024
the same total counting down: 5050
Notice the starting values. A sum starts at 0, because adding 0 changes nothing. A product starts at 1, because multiplying by 1 changes nothing. Start a product at 0 and your answer is 0 no matter what follows, which is a bug that has caught everyone at least once. A counter starts at 0 too.
Notice also where sum was declared: above the loop. Put int sum = 0; inside the body and it is reset to zero on every trip, so the final answer is the last value rather than the total, and the compiler will not complain because nothing is illegal about it. The third loop shows the other common shape, where the loop runs until a quantity crosses a threshold rather than a fixed number of times, and the count of trips is itself the answer you wanted.
The last line is a small proof of correctness. Adding 1 to 100 upwards and downwards gives the same 5050, which is the answer Gauss is said to have found as a schoolboy by pairing the ends, and the agreement between the two loops is a cheap check that neither has a bounds mistake.
Common misconceptions
- "for (int i = 0; i <= 5; i++) runs five times." It runs six. With
<=you add one to the difference of the bounds. Fence.java printed both counts side by side. - "The update runs before the body." It runs after. Order.java showed the body seeing 1, 2 and 3 while the variable ended at 4.
- "A loop always runs at least once." Both while and for test first, so a condition that starts false gives zero trips, and that is often the correct behaviour rather than a bug.
- "A loop with a counter, a condition and an update cannot be infinite." Runaway.java had all three. Because the step was 2 and the test was
!=, the counter jumped straight over the value it was waiting for. - "Declaring the total inside the loop is the same as declaring it above." Inside the loop it is reset on every trip, so you get the last value instead of the accumulated one.
- "i++ and i = i + 1 behave differently." As a statement on its own they are identical. The exam avoids
++used inside a larger expression, so you should too.
Pulling it together
A while loop is a condition and a body; a for loop is the same thing with the setup, the condition and the change collected in one pair of brackets. Both test before the body, so both can run zero times, and in both the change happens after the body, leaving the counter one step past the last value the body saw. Trip counts follow the fence-post arithmetic: upper minus lower for <, one more than that for <=. Getting it wrong by one produces a program that works perfectly until the last element and then throws an out-of-bounds exception, which is the most recognisable failure in this course. Prefer an inequality to != in a loop condition so the counter cannot step over the end. And build answers with the accumulator pattern: declare above, update inside, read afterwards, starting a sum at 0 and a product at 1.
The core of it: before you run a loop, say out loud how many times its body will execute. If you cannot, you do not yet know what the loop does.
Next lesson puts one loop inside another, which is how you print a grid, compare every pair in a list, and walk a String one character at a time doing something with each. The counting gets harder, and the tracing habit you have just built is what will save you.
Sources
- Oracle. (n.d.). The Java Tutorials: The while and do-while Statements (a while statement evaluates its expression before each pass, so the body may run zero times). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: The for Statement (the initialisation, termination and increment expressions, and the scope of a counter declared in the header). docs.oracle.com
- Wikipedia contributors. (2026). Off-by-one error (the fence-post form of the mistake and its appearance in loop bounds). en.wikipedia.org
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 2, topics 2.7 while Loops and 2.8 for Loops. apcentral.collegeboard.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 6: Loops and Strings. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- while loop
- A condition and a body. The condition is tested before every pass, so the body may run zero times.
- for loop
- A loop whose setup, condition and change sit together in the brackets of its header.
- Loop control variable
- The counter a loop tests and changes. Declared in a for header, it stops existing when the loop ends.
- Off-by-one error
- Running a loop one trip too many or too few, usually by writing <= where < was meant.
- Infinite loop
- A loop whose condition never becomes false, often because the step jumps over the value being tested for.
- Accumulator
- A variable declared before a loop, updated inside it, and read after it, holding a running total, product or count.
- Fence-post rule
- A loop from a to b testing with < makes b minus a trips; testing with <= it makes one more.
Nested loops and walking a string
- Write a nested loop and say how many times its innermost body runs.
- Traverse a String one character at a time using substring within the exam's method subset.
- Build a reversed String and test a String for being a palindrome.
- Recognise when an inner loop's bound depends on the outer loop's variable.
Fifteen stars in three rows
Print a rectangle of stars, three rows deep and five across. One loop gets you a single row. To get three rows you need a loop whose body contains a loop, which is called nested iteration, and it is the single most useful structure in the second half of this course, because a grid, a table, a seating plan and every pairwise comparison all have this shape.
public class Grid {
public static void main(String[] args) {
for (int row = 1; row <= 3; row++) {
for (int col = 1; col <= 5; col++) {
System.out.print("*");
}
System.out.println();
}
System.out.println("---");
for (int row = 1; row <= 4; row++) {
for (int col = 1; col <= row; col++) {
System.out.print("*");
}
System.out.println();
}
System.out.println("---");
for (int row = 1; row <= 3; row++) {
for (int col = 1; col <= 5; col++) {
System.out.print(row + "" + col + " ");
}
System.out.println();
}
}
}
*****
*****
*****
---
*
**
***
****
---
11 12 13 14 15
21 22 23 24 25
31 32 33 34 35
Follow the first shape as Java did. The outer loop sets row to 1 and enters its body. The body's first statement is a whole loop, which runs from col 1 to col 5 printing five stars with no line breaks. That loop finishes. The next statement, System.out.println() with nothing in it, ends the line. Now the outer body is done, so row becomes 2 and the whole inner loop starts again from col 1.
The one sentence to remember is this: the inner loop starts over from the beginning every single time the outer body runs. It does not carry on from where it stopped. A for loop's initialisation part runs each time control reaches the loop, and control reaches this loop three times.
The placement of the empty println also matters, and it is the commonest slip. It sits inside the outer loop and outside the inner one, which is what makes one line break per row. Put it inside the inner loop and you get fifteen lines of one star each. Put it after the outer loop and you get one line of fifteen stars.
The second shape shows the trick that makes triangles: the inner bound is col <= row, so it depends on the outer variable. Row 1 prints one star, row 4 prints four. The third shape prints the row and column number of every cell, which is exactly the picture you want in your head when two-dimensional arrays arrive in Module 5.
Aligning the output, which forces an if inside two loops
A multiplication table is the same nesting with arithmetic in the middle, and it raises a problem worth solving now: numbers of different widths ruin the columns.
public class Table {
public static void main(String[] args) {
for (int row = 1; row <= 5; row++) {
for (int col = 1; col <= 5; col++) {
int product = row * col;
if (product < 10) {
System.out.print(" " + product + " ");
} else {
System.out.print(product + " ");
}
}
System.out.println();
}
}
}
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
4 8 12 16 20
5 10 15 20 25
Three levels deep: a loop, a loop, an if. Single-digit products get an extra leading space so every cell occupies three characters, and the columns line up. This is worth doing once by hand because it forces you to see that the innermost statement runs twenty-five times and the condition is tested twenty-five times, while the line break happens five times.
Counting the trips, including when the bounds are linked
Exam questions about nested loops are usually counting questions. Three patterns cover nearly all of them, and here is Java counting each for itself.
public class Counting {
public static void main(String[] args) {
int trips = 0;
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= 6; j++) {
trips++;
}
}
System.out.println("4 outer by 6 inner ran the body " + trips + " times");
trips = 0;
int outerTrips = 0;
for (int i = 1; i <= 5; i++) {
outerTrips++;
for (int j = 1; j <= i; j++) {
trips++;
}
}
System.out.println("triangular loop: outer ran " + outerTrips
+ " times, inner body ran " + trips + " times");
trips = 0;
for (int i = 0; i < 5; i++) {
for (int j = i + 1; j < 5; j++) {
trips++;
}
}
System.out.println("every pair from five items, counted once: " + trips);
}
}
4 outer by 6 inner ran the body 24 times
triangular loop: outer ran 5 times, inner body ran 15 times
every pair from five items, counted once: 10
When the two bounds are independent, multiply them: four times six is twenty-four. When the inner bound is the outer variable, the counts are 1 + 2 + 3 + 4 + 5, which is 15, the fifth triangular number. When the inner loop starts at i + 1, you get every unordered pair exactly once: ten pairs from five items, which is 4 + 3 + 2 + 1. That last shape is the one selection sort and insertion sort use in Module 7, and knowing it is 10 rather than 25 is the difference between understanding why those sorts cost what they cost and merely reciting it.
Why this matters: a nested loop over n items usually does something like n squared units of work, and that is why a program that is instant on 10 items can be unusable on 10,000.
Walking a String one character at a time
You have used substring to cut a String at a known place. Put it in a loop and it becomes a way to look at every character in turn.
public class Walk {
public static void main(String[] args) {
String phrase = "Ada Lovelace";
for (int i = 0; i < phrase.length(); i++) {
String letter = phrase.substring(i, i + 1);
System.out.println("index " + i + ": [" + letter + "]");
}
int vowels = 0;
int spaces = 0;
String vowelSet = "aeiouAEIOU";
for (int i = 0; i < phrase.length(); i++) {
String letter = phrase.substring(i, i + 1);
if (vowelSet.indexOf(letter) >= 0) {
vowels++;
}
if (letter.equals(" ")) {
spaces++;
}
}
System.out.println("vowels " + vowels + ", spaces " + spaces);
int eCount = 0;
for (int i = 0; i < phrase.length(); i++) {
if (phrase.substring(i, i + 1).equals("e")) {
eCount++;
}
}
System.out.println("lowercase e appears " + eCount + " times");
}
}
index 0: [A]
index 1: [d]
index 2: [a]
index 3: [ ]
index 4: [L]
index 5: [o]
index 6: [v]
index 7: [e]
index 8: [l]
index 9: [a]
index 10: [c]
index 11: [e]
vowels 6, spaces 1
lowercase e appears 2 times
Three things in this program are the standard idioms, and you should copy them exactly.
First, substring(i, i + 1) is how you get the single character at index i as a String. Java does have a charAt method that returns a char, but char is not one of the three primitive types this exam uses, and charAt is not on the Java Quick Reference you are given, so the whole course stays with substring. The bounds are the usual ones: from i, up to but not including i + 1, which is one character long.
Second, the loop condition is i < phrase.length(). Not <=. Lesson 6 showed you what <= costs here.
Third, the test for membership of a set: rather than five separate comparisons for the vowels, the program keeps them in a String and asks vowelSet.indexOf(letter) >= 0, which means "letter occurs somewhere in vowelSet". Both cases are in that set, which is why it found six vowels in Ada Lovelace, counting the A at the start. Testing for one specific character uses equals, because the character is a String and == would be asking the wrong question, exactly as in Lesson 3.
Building a new String backwards
Reversing text is the standard exercise for this material, and there are two ways round, both worth seeing.
public class Mirror {
public static void main(String[] args) {
String word = "stressed";
String backwards = "";
for (int i = word.length() - 1; i >= 0; i--) {
backwards = backwards + word.substring(i, i + 1);
}
System.out.println(word + " reversed is " + backwards);
String other = "";
for (int i = 0; i < word.length(); i++) {
other = word.substring(i, i + 1) + other;
}
System.out.println("built the other way round: " + other);
}
}
stressed reversed is desserts
built the other way round: desserts
The first loop walks the original from the last index down to 0, sticking each character on the end of the answer. Note the starting value: word.length() - 1, because the last valid index is one less than the length, and the condition is >= 0 so that index 0 is included. The second loop walks forwards but puts each new character in front of what it has, which reverses the string without ever counting down. Both are accumulators, exactly as in Lesson 6, but accumulating text rather than a number, which is why each starts from the empty String "" rather than from 0.
The same program, one letter different
Now the test that makes tracing worthwhile. A palindrome reads the same in both directions, so you can compare the character at index i with the character the same distance from the other end.
String first = "level";
boolean same = true;
for (int i = 0; i < first.length(); i++) {
String left = first.substring(i, i + 1);
String right = first.substring(first.length() - 1 - i, first.length() - i);
if (!left.equals(right)) {
same = false;
}
}
System.out.println(first + " is a palindrome: " + same);
The whole program ran that block three times over, on level, then on levels, then on Level:
level is a palindrome: true
levels is a palindrome: false
Level is a palindrome: false
Work out the mirror index once and you never have to again. For a String of length n, the partner of index i is index n - 1 - i. Check it on level: n is 5, index 0 pairs with 4, index 1 with 3, index 2 with itself. The substring call needs both ends, so it asks for n - 1 - i up to n - i, a single character.
Adding one letter flipped the answer, which is the behaviour you wanted. Changing one letter's case also flipped it, and that is worth arguing about: "L".equals("l") is false, because capitals and lowercase letters are different characters, so by the strict reading of the program Level is not a palindrome. A human would say it is. Which answer is right depends on the specification you were given, and on a free-response question the specification is the only authority. Read it twice.
One more thing about this loop: it does twice the work it needs to. Once i passes the middle it is comparing the same pairs again in the other order. Stopping at i < first.length() / 2 gives the same answer in half the trips. Both are correct; only one shows you were thinking.
Common misconceptions
- "The inner loop carries on from where it left off." It restarts from its initial value every time the outer body runs. That is why the rectangle had five stars on every row rather than five, then none.
- "A nested loop over 5 by 5 runs the body 10 times." Independent bounds multiply: 25. It is 10 only when the inner loop starts at
i + 1, which counts each pair once. - "The line break goes inside the inner loop." Then you get one item per line. It belongs inside the outer loop and after the inner one.
- "substring(i, i) gives the character at i." It gives the empty String, because the second argument is excluded. You want
substring(i, i + 1). - "Use == to check whether a character matches." The pieces are Strings, so
==compares references. Useequals, as Lesson 3 established. - "A reversal loop starts at word.length()." It starts at
word.length() - 1. Starting at the length throws StringIndexOutOfBoundsException on the first trip.
The takeaway
A nested loop is a loop in the body of another loop, and the inner one restarts completely on every pass of the outer one. When the bounds are independent, multiply them to count the trips; when the inner bound is the outer variable you get a triangular count; when the inner loop starts at one past the outer variable you get each pair once. A String is walked with for (int i = 0; i < s.length(); i++) and read one character at a time with s.substring(i, i + 1), which keeps you inside the method subset the exam gives you. Text accumulates into a String that starts as "", the last index of a String is its length minus one, and the mirror of index i is index n - 1 - i.
Bottom line: when a nested loop confuses you, print the loop variables. Two lines of tracing output beat twenty minutes of staring.
That closes Module 3. You can now make a program decide and make it repeat, which is everything a program can do to a value. Module 4 changes what the values are: instead of using classes other people wrote, you start writing your own, beginning with a class designed on the page from a written description.
Sources
- Oracle. (n.d.). The Java Tutorials: The for Statement (the initialisation expression is evaluated once each time control enters the loop, which is what makes an inner loop restart). docs.oracle.com
- College Board. (2025). AP Computer Science A: 2026 Exam Reference Information, Java Quick Reference (the String methods available on the exam: length, both forms of substring, indexOf, equals, compareTo and split; charAt is not among them). apcentral.collegeboard.org
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 2, topics 2.10 Implementing String Algorithms and 2.11 Nested Iteration. apcentral.collegeboard.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 6: Loops and Strings. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Nested iteration
- A loop whose body contains another loop. The inner loop restarts on every pass of the outer one.
- Inner loop
- The loop written inside another loop's body; its counter is set up again each time control reaches it.
- Dependent bound
- An inner loop whose limit uses the outer loop's variable, which produces a triangular count of trips.
- String traversal
- Visiting every character of a String with a loop from 0 up to but not including its length.
- Palindrome
- Text that reads the same forwards and backwards. Index i pairs with index n minus 1 minus i.
- String accumulator
- A String starting as the empty String and grown inside a loop, the text version of a running total.
- Membership test
- Asking whether one piece of text occurs in another with indexOf and a test for a result of at least zero.
Module 4: Writing Your Own Classes
Two lessons that move you from user to author. The first designs a class from a written request, one decision at a time: which fields, which constructor, which methods, and what private actually protects, with the compiler's refusal shown in full. The second covers the keywords that make a class read properly, this and static, the scope rules that decide which name wins, and the toString convention that turns an object into a line of text.
Designing a class on the page: fields, constructors, accessors, mutators
- Turn a written description into a class with private fields and public methods.
- Write a constructor, including a second one that supplies a default value.
- Write accessor and mutator methods, and validate a value before storing it.
- Explain what private prevents, and what happens when two variables refer to one object.
A note from the librarian
Here is the whole specification, in the form such things actually arrive in.
We need to keep track of each book: its title, its author, and how many copies are on the shelf. Staff need to be able to look up any of those. When someone borrows a copy the shelf count should go down by one, but not below zero, and the system should tell us whether the loan succeeded. Returns put a copy back. Nobody outside the system should be able to set the shelf count to a negative number.
That paragraph contains a complete class, and the job of this lesson is to get it out. A class is a description of a kind of thing; an object is one actual thing built from that description. One Book class, thousands of book objects, each with its own title and its own count. Oracle's tutorial on classes calls the class a blueprint, and that is exactly right: you do not live in a blueprint.
Three questions get you from a paragraph to code, and they are worth asking in this order every time.
| Question | What it produces | From the librarian's note |
|---|---|---|
| What must each one remember? | fields | title, author, copies on the shelf |
| What must be true the moment one exists? | the constructor | a title, an author, and a starting count |
| What can be done to one? | methods | read each field, borrow, return, correct the count |
Notice what is not in the list. There is no field for the borrower's name, because the note never mentions one, and no method to print a receipt. Inventing members that nobody asked for is the commonest way to lose marks on an exam question about class design, and the commonest way to waste a week at work.
Fields, and the word private
A field, also called an instance variable, is a variable declared inside the class but outside every method. Each object gets its own copy.
public class Book {
private String title;
private String author;
private int copiesOnShelf;
The word private means: only code inside this class may touch this variable. That is not a courtesy, it is the mechanism that lets the class keep its promises. The librarian said the count must never go negative. If anything anywhere could write b.copiesOnShelf = -99; then that promise is worth nothing, because the class has no way to police it. Make the fields private and every change has to go through a method you wrote, where you can check it first. The name for this arrangement is encapsulation, and the rule for this exam is simple: fields private, methods public unless you have a reason.
Fields differ from local variables in a second way, which surprises people. A local variable that you never assign cannot be used; the compiler refuses. A field that you never assign has a default value. Here is a class with four fields and no constructor at all.
public class Empty {
private String label;
private int count;
private double rate;
private boolean ready;
public String report() {
return "label " + label + ", count " + count + ", rate " + rate + ", ready " + ready;
}
}
public class Show {
public static void main(String[] args) {
Empty e = new Empty();
System.out.println(e.report());
}
}
Compiled together and run, that prints:
label null, count 0, rate 0.0, ready false
So an int field starts at 0, a double at 0.0, a boolean at false, and any reference field at null. Two consequences follow. First, you can write new Empty() even though Empty has no constructor, because Java supplies a default constructor with no parameters when you write none yourself. Second, the moment you write any constructor of your own, that free one disappears, so if you write Book(String, String, int) then new Book() no longer compiles.
The point: null is the default for every object field, and a field left at null is where most NullPointerExceptions are born.
The constructor, and then a second one
A constructor runs once, when new builds the object, and its job is to put the fields into a sensible state. It has the same name as the class and no return type, not even void.
public Book(String bookTitle, String bookAuthor, int copies) {
title = bookTitle;
author = bookAuthor;
copiesOnShelf = copies;
}
public Book(String bookTitle, String bookAuthor) {
title = bookTitle;
author = bookAuthor;
copiesOnShelf = 1;
}
Two constructors, differing in how many parameters they take. That is legal, and it is called overloading: Java decides which one you meant by counting and typing the arguments in the call. new Book("The Double Helix", "James Watson") matches the second and quietly supplies a count of 1. Oracle's page on providing constructors for your classes notes that a class can have several as long as their parameter lists differ.
Look at the parameter names. They are bookTitle and bookAuthor, not title and author. If a parameter had the same name as a field, the parameter would win inside the constructor and title = title; would assign the parameter to itself, leaving the field null. There is a keyword that fixes that, this, and it is the first thing in the next lesson. For now, different names avoid the problem entirely.
Accessors and mutators
An accessor reports a field without changing anything. It has a return type and a return statement, and by convention its name starts with get, or with is when the answer is a boolean.
public String getTitle() {
return title;
}
public int getCopiesOnShelf() {
return copiesOnShelf;
}
public boolean isAvailable() {
return copiesOnShelf > 0;
}
isAvailable is worth a second look, because it returns something no field holds. The class knows the count; whether the book is available is a conclusion drawn from the count. Working a value out on demand is usually better than storing it, because a stored copy can fall out of step with the thing it was copied from.
A mutator changes a field. Its return type is often void, but it does not have to be, and here the librarian asked for something more interesting: borrowing should report whether it worked.
public boolean borrowCopy() {
if (copiesOnShelf > 0) {
copiesOnShelf = copiesOnShelf - 1;
return true;
}
return false;
}
public void returnCopy() {
copiesOnShelf = copiesOnShelf + 1;
}
public void setCopiesOnShelf(int copies) {
if (copies >= 0) {
copiesOnShelf = copies;
}
}
Three patterns are on display. borrowCopy guards the change with a condition and reports the outcome, so the caller can react. returnCopy takes no parameter because the amount is always one. And setCopiesOnShelf is a mutator method that validates before it stores: a negative argument is ignored, which is precisely the promise the private field made possible.
Once return runs, the method stops. That is why borrowCopy needs no else: reaching the final return false; is only possible when the condition was false.
The class, driven by a second class, with its real output
A Java file holds one public class, so Book and the program that uses it live in two files, compiled together. Here is the driver.
public class Main {
public static void main(String[] args) {
Book atlas = new Book("A Brief History of Time", "Stephen Hawking", 3);
Book rare = new Book("The Double Helix", "James Watson");
System.out.println(atlas.getTitle() + " by " + atlas.getAuthor());
System.out.println("copies on the shelf: " + atlas.getCopiesOnShelf());
System.out.println(rare.getTitle() + " has " + rare.getCopiesOnShelf() + " copy");
System.out.println("borrowing the Hawking three times:");
System.out.println(atlas.borrowCopy() + " leaves " + atlas.getCopiesOnShelf());
System.out.println(atlas.borrowCopy() + " leaves " + atlas.getCopiesOnShelf());
System.out.println(atlas.borrowCopy() + " leaves " + atlas.getCopiesOnShelf());
System.out.println("a fourth attempt: " + atlas.borrowCopy());
System.out.println("available? " + atlas.isAvailable());
atlas.returnCopy();
System.out.println("after one return: " + atlas.getCopiesOnShelf()
+ ", available? " + atlas.isAvailable());
System.out.println("trying to set a negative count:");
atlas.setCopiesOnShelf(-5);
System.out.println("copies are still " + atlas.getCopiesOnShelf());
atlas.setCopiesOnShelf(12);
System.out.println("a legal change gives " + atlas.getCopiesOnShelf());
System.out.println("the other book is untouched: " + rare.getCopiesOnShelf());
Book alias = rare;
alias.borrowCopy();
System.out.println("after borrowing through alias, rare has " + rare.getCopiesOnShelf());
}
}
Compiled with javac on both files and run, it printed:
A Brief History of Time by Stephen Hawking
copies on the shelf: 3
The Double Helix has 1 copy
borrowing the Hawking three times:
true leaves 2
true leaves 1
true leaves 0
a fourth attempt: false
available? false
after one return: 1, available? true
trying to set a negative count:
copies are still 1
a legal change gives 12
the other book is untouched: 1
after borrowing through alias, rare has 0
Read the middle. Three borrowings returned true and walked the count down 2, 1, 0. The fourth returned false and, crucially, did not take the count to minus one. The class kept its promise, and the caller could tell. Then the negative argument to the mutator was ignored while the legal one went through. Every guarantee in the librarian's note now holds, not because everyone is careful but because the class makes breaking it impossible from outside.
The line about the other book is the one people underestimate: borrowing from the Hawking left the Watson at 1. Each object has its own fields, so a method called on one object touches nothing else. That is what makes objects worth having.
What private actually prevents
To show the protection is real, here is a second program that tries to bypass it. Book is unchanged.
public class Peek {
public static void main(String[] args) {
Book b = new Book("Silent Spring", "Rachel Carson", 2);
System.out.println(b.copiesOnShelf);
b.copiesOnShelf = -99;
}
}
What javac really said:
Peek.java:4: error: copiesOnShelf has private access in Book
System.out.println(b.copiesOnShelf);
^
Peek.java:5: error: copiesOnShelf has private access in Book
b.copiesOnShelf = -99;
^
2 errors
Two errors, one for reading and one for writing, with the caret on the dot in each case. No class file was produced, so there was never a program to run. Oracle's page on controlling access to members of a class sets out the levels; for this exam you need two of them, public and private, and the habit of choosing private first.
Two names, one object
The last two lines of Main are a callback to Lesson 3, and they are the reason that lesson insisted a reference variable holds a way to reach an object rather than the object itself.
Book alias = rare;
alias.borrowCopy();
System.out.println("after borrowing through alias, rare has " + rare.getCopiesOnShelf());
after borrowing through alias, rare has 0
Assigning rare to alias copied the reference, not the book. There is still exactly one Book object, with two names pointing at it, so borrowing through one name is visible through the other. With a String this could never bite you, because a String cannot be changed at all; with a class of your own, whose methods do change its fields, it can and will. When a method takes an object as a parameter, the same thing happens: the method gets a second reference to your object and any mutator it calls changes the object you passed in.
Common misconceptions
- "A constructor returns the object, so it needs a return type." A constructor has no return type at all, not even void. Writing
public void Book(...)makes it an ordinary method named Book, and thennew Book(...)will not find a matching constructor. - "Every class gets a no-argument constructor." Only a class that declares no constructor of its own. Once Book declares one,
new Book()is a compile error. - "private stops the object's own methods from using the field." It stops code outside the class. Inside Book, every method reads and writes copiesOnShelf freely; that is the whole design.
- "A field must be given a value or it is an error." Fields get defaults: 0, 0.0, false, and null. Local variables do not, which is why the compiler treats the two differently.
- "A mutator has to return void." It can return anything useful. borrowCopy returns a boolean saying whether the loan happened, and that is better design than a void method that fails silently.
- "Copying an object variable copies the object." It copies the reference. Main proved it: a change made through alias showed up through rare, because there was only ever one object.
What you now know
A class is a description; an object is one thing built from it. Fields hold what each object remembers, they are private so that the class can enforce its own rules, and they start at 0, 0.0, false or null. A constructor has the class's name, no return type, and the job of leaving the fields in a state that makes sense; a second constructor with a different parameter list can fill in a default. Accessors return information without changing it, mutators change a field and may validate first or report the outcome, and a value that can be worked out, like availability, is better computed than stored. Two files compile together with a single javac command, and two variables can name one object, so a mutator called through either is visible through both.
So what?: Question 2 of the free-response section is exactly this task. You are given a paragraph and a table of interactions, and you write the class header, the fields, a constructor and a method. The three questions at the top of this lesson are the procedure.
Next lesson finishes the toolkit: the this keyword that lets a parameter share a field's name, the static members that belong to the class rather than to any object, the scope rules that decide which variable a name refers to, and the toString method that decides what your object looks like when it is printed.
Sources
- Oracle. (n.d.). The Java Tutorials: Classes (the class as a blueprint, field declarations, and the shape of a class body). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Providing Constructors for Your Classes (constructors take the class name and no return type, a class may declare several with different parameter lists, and the compiler supplies a no-argument constructor only when none is declared). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Controlling Access to Members of a Class (what the private and public modifiers permit). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 3, topics 3.3 Anatomy of a Class, 3.4 Constructors and 3.5 Methods: How to Write Them; and the free-response Question 2 description of class design. apcentral.collegeboard.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 11: Designing Classes. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Class
- A description of a kind of thing: its fields and its methods. Not itself a thing.
- Field
- A variable declared in the class and outside every method, also called an instance variable. Each object has its own.
- private
- An access modifier allowing only code inside the same class to read or write the member.
- Constructor
- A method with the class's name and no return type, run by new to set the fields up.
- Default constructor
- The no-argument constructor Java supplies only when the class declares no constructor of its own.
- Accessor
- A method that returns information about the object without changing it, usually named get or is.
- Mutator
- A method that changes a field, often after checking that the new value is allowed.
- Encapsulation
- Keeping data private and exposing it only through methods, so the class can enforce its own rules.
- Alias
- A second reference variable pointing at the same object, so a change through one is visible through the other.
this, static, scope, and the toString convention
- Diagnose a constructor whose parameter hides a field, and repair it with this.
- Distinguish a static member from an instance member and say which belongs to what.
- Apply Java's scope rules to decide where a declared name can be used.
- Write a toString method and explain when Java calls it for you.
A student whose name came out null
This class looks like the one you wrote last lesson. Its fields are private, its constructor takes both values, its accessors return them. It compiles with no errors and no warnings.
public class Student {
private String name;
private int year;
public Student(String name, int year) {
name = name;
year = year;
}
public String getName() {
return name;
}
public int getYear() {
return year;
}
}
public class Broken {
public static void main(String[] args) {
Student s = new Student("Nia Okafor", 11);
System.out.println("name is " + s.getName());
System.out.println("year is " + s.getYear());
System.out.println("length of the name: " + s.getName().length());
}
}
Compiled with javac on both files, and run:
name is null
year is 0
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "Student.getName()" is null
at Broken.main(Broken.java:6)
Nia Okafor went in and null came out, the year 11 went in and 0 came out, and the third line crashed. Those are exactly the default values from last lesson, which is the clue: the fields were never assigned at all.
Here is why. Inside the constructor there are two things called name: the parameter and the field. When a local name and a field share a spelling, the nearer one wins, and a parameter is as near as it gets. So name = name; reads the parameter and writes the parameter. The field is not involved. It is a legal statement that does nothing, and the compiler has no way of knowing you meant something else.
The technical term is that the parameter shadows the field. The bug is not rare and it is not a beginner's bug: it happens whenever someone gives a constructor parameter the obvious name.
this, the reference an object has to itself
Inside any instance method or constructor, the keyword this is a reference to the object the method was called on. So this.name is unambiguous: it is the field, whatever any local name happens to be spelled. Oracle's page on using the this keyword gives this as its main use, and notes that the most common reason is exactly the shadowing above.
public class Student {
private String name;
private int year;
public Student(String name, int year) {
this.name = name;
this.year = year;
}
public String getName() {
return name;
}
public int getYear() {
return year;
}
public boolean sameYearAs(Student other) {
return this.year == other.year;
}
public String toString() {
return name + " (year " + year + ")";
}
}
Two characters changed, and the output changed completely:
name is Nia Okafor
year is 11
length of the name: 10
s and t in the same year? true
s and u in the same year? false
printed directly: Nia Okafor (year 11)
Ivy Chen (year 13)
Read this.name = name; out loud as "this object's name gets the parameter called name" and the line stops looking strange. Note that the accessors still write plain name, with no this, and they work: when nothing local shares the spelling, the field is the only candidate. Both styles are correct, and the exam accepts either.
The sameYearAs method shows the second use of this, and it is the one that makes the idea click. The method takes another Student and compares two objects: this.year is the year of the object the method was called on, and other.year is the year of the object passed in. Calling s.sameYearAs(t) makes this be s. And notice that other.year is legal even though year is private, because private means private to the class, not to the object: a Student may read another Student's fields.
Key idea: this is the answer to "which object are we talking about?", and every instance method has one.
static: one copy for the whole class
Now a different question. Suppose a box office wants to number tickets 1, 2, 3 as it sells them. No single ticket can hold that number, because the count belongs to the whole sale, not to any one ticket. That is what static is for: a static variable exists once, in the class, no matter how many objects there are.
public class Ticket {
private static int issued = 0;
private static final int PRICE_IN_PENCE = 250;
private int number;
private String holder;
public Ticket(String holder) {
issued = issued + 1;
this.number = issued;
this.holder = holder;
}
public int getNumber() {
return number;
}
public String getHolder() {
return holder;
}
public static int getIssued() {
return issued;
}
public static int totalTakenInPence() {
return issued * PRICE_IN_PENCE;
}
public String toString() {
return "ticket " + number + " for " + holder;
}
}
public class Gate {
public static void main(String[] args) {
System.out.println("issued before anyone arrives: " + Ticket.getIssued());
Ticket a = new Ticket("Priya");
Ticket b = new Ticket("Marcus");
Ticket c = new Ticket("Lena");
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println("a.getNumber() is " + a.getNumber()
+ " and c.getNumber() is " + c.getNumber());
System.out.println("issued, read through the class: " + Ticket.getIssued());
System.out.println("total taken in pence: " + Ticket.totalTakenInPence());
}
}
issued before anyone arrives: 0
ticket 1 for Priya
ticket 2 for Marcus
ticket 3 for Lena
a.getNumber() is 1 and c.getNumber() is 3
issued, read through the class: 3
total taken in pence: 750
The first line ran before any Ticket existed, and it worked, because Ticket.getIssued() is a static method called through the class name. That is the same shape as Math.sqrt from Lesson 4, and now you know why it is written that way.
The constructor does something worth studying: it increments the shared count and then copies the new value into that object's own number. So issued is one variable that keeps climbing, while each ticket's number is frozen at whatever it was when that ticket was built, which is why a still reads 1 after two more tickets were sold.
PRICE_IN_PENCE is static final: static because every ticket costs the same, final because it must not change. A final variable can be assigned once and then never again, and the all-capitals name with underscores is the convention for such a constant. Oracle's page on understanding class members sets out both keywords.
| Instance member | Static member | |
|---|---|---|
| How many copies | one per object | one per class |
| Called through | a reference: a.getNumber() | the class: Ticket.getIssued() |
| Has a this | yes | no |
| Can read instance fields | yes | no, not without an object |
| Exists before any object | no | yes |
| Typical use | the state of one thing | a shared count, a constant, a utility |
The one thing a static method cannot do
The fourth row of that table is worth a compiler error of its own, because it explains a message you will certainly meet.
public class Meter {
private int reading;
public Meter(int reading) {
this.reading = reading;
}
public static int doubled() {
return reading * 2;
}
}
Meter.java:9: error: non-static variable reading cannot be referenced from a static context
return reading * 2;
^
1 error
A static method has no this, so when it writes reading there is no object whose reading could be meant. The fix is either to drop static, making it an instance method, or to pass a Meter in as a parameter. This is also the answer to a question you may have been carrying since Lesson 1: main is static because it has to run before any object exists, and that is why a static main cannot reach an instance field directly.
Scope: where a name exists at all
Scope is the region of code in which a declared name can be used, and Java's rule is short: a local variable lives from its declaration to the closing brace of the block containing it.
public class Scope {
public static void main(String[] args) {
int total = 0;
for (int i = 1; i <= 3; i++) {
int doubled = i * 2;
total = total + doubled;
System.out.println("inside the loop: i " + i + ", doubled " + doubled);
}
System.out.println("after the loop, total is " + total);
System.out.println("doubled is " + doubled);
}
}
Scope.java:10: error: cannot find symbol
System.out.println("doubled is " + doubled);
^
symbol: variable doubled
location: class Scope
1 error
doubled was declared inside the loop body, so it ceased to exist at the loop's closing brace, and the last line cannot see it. Neither can it see i, which was declared in the for header. total was declared in the method body, so it survives the loop, which is exactly why accumulators have to be declared above their loop.
Note the shape of that error. cannot find symbol with symbol: variable underneath is Java's way of saying "you used a name I have never heard of here", and it has three usual causes: a typo, a missing declaration, or a variable used outside its scope. Fields are the exception to all of this: a field is in scope in every method of its class, which is precisely what makes it a field.
toString: what your object looks like when printed
Here are two classes that store the same two numbers. One declares a toString method and the other does not.
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
public class Labelled {
private int x;
private int y;
public Labelled(int x, int y) {
this.x = x;
this.y = y;
}
public String toString() {
return "(" + x + ", " + y + ")";
}
}
public class Compare {
public static void main(String[] args) {
Point p = new Point(3, 4);
Labelled q = new Labelled(3, 4);
System.out.println("Point with no toString: " + p);
System.out.println("Labelled with a toString: " + q);
System.out.println(q);
String text = "the point is " + q + " and that is all";
System.out.println(text);
System.out.println("reading the fields by hand: (" + p.getX() + ", " + p.getY() + ")");
}
}
One real run of it:
Point with no toString: Point@372f7a8d
Labelled with a toString: (3, 4)
(3, 4)
the point is (3, 4) and that is all
reading the fields by hand: (3, 4)
The first line is the one to understand. Nobody asked Java to print anything about a Point, yet joining a Point to a String produced Point@372f7a8d. Every class in Java inherits a toString method from the Object class, and the inherited version prints the class name, an at sign, and a number derived from where the object sits in memory. The number will be different on your machine and different on the next run of mine, which is a useful thing to know: if your output contains an at sign and some hexadecimal, you have printed an object that has no toString of its own.
Labelled printed (3, 4) in all three places it appeared, and nowhere in the driver does the word toString occur. That is the convention: whenever an object is joined to a String with +, or handed to System.out.println, Java calls its toString for you. Writing one is how you make your own class print like a value instead of like a memory address.
Two practical notes. The method must be spelled exactly public String toString(), with no parameters, or Java will not find it and you will get the at sign again. And the current College Board framework requires you to know that concatenation calls toString and that subclasses commonly replace it, while placing the act of writing a replacement outside the exam's scope. Write them anyway. Your own code becomes far easier to debug, and the habit costs nothing.
Common misconceptions
- "name = name; in a constructor assigns the parameter to the field." It assigns the parameter to itself. Broken.java printed null and 0, then threw NullPointerException.
- "You must write this in front of every field." Only when something local shares the spelling. The accessors in the fixed Student use a bare
nameand are correct. - "A static field belongs to the first object created." It belongs to the class and exists before any object. Gate.java read the count successfully before building a single Ticket.
- "A static method can use the fields of its class." It cannot, because it has no object. That is the non-static variable cannot be referenced from a static context error, and it is why main takes objects as parameters or creates them.
- "A variable declared in a loop can be read after the loop." Its scope ends at the closing brace, giving cannot find symbol. Declare it above the loop if you need it afterwards.
- "An object prints its fields unless you stop it." Without a toString of your own it prints the class name, an at sign and a hexadecimal number, as Point did.
Summing up
When a parameter and a field share a name, the parameter wins, and this.field = parameter; is the repair. Inside an instance method, this refers to the object the method was called on, which also lets a method compare itself with another object of the same class and reach that object's private fields. A static member belongs to the class rather than to any object: one copy, reached through the class name, existing before the first object and unable to touch instance fields, which is why main is static and why Math's methods are. static final with an all-capitals name is how you write a constant. A local variable's scope runs to the closing brace of its block, so accumulators are declared above their loops, while a field is in scope throughout its class. And Java calls toString for you whenever an object meets a String or a println, so a class without one prints an at sign and an address.
What matters here: the compiler catches nothing in this lesson except the static-context error and the scope error. The shadowing bug and the missing toString both compile cleanly and produce wrong output, which is why you read output rather than trusting a clean build.
That closes Module 4. You can now write a class from a description and make it behave properly when printed. Module 5 gives you the containers: the array, which holds a fixed number of values; the ArrayList, which grows; and the two-dimensional array, which is a grid. Every standard algorithm on the exam lives in that module.
Sources
- Oracle. (n.d.). The Java Tutorials: Using the this Keyword (this refers to the current object, and its commonest use is a field shadowed by a parameter). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Understanding Class Members (class variables and class methods belong to the class, are reached through the class name, and cannot refer to instance members; final for constants). docs.oracle.com
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class Object (the inherited toString returns the class name, an at sign, and an unsigned hexadecimal representation of the hash code). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 3, topics 3.7 Class Variables and Methods, 3.8 Scope and Access, 3.9 this Keyword; and Unit 1, essential knowledge 1.15.A.5 with its exclusion statement about overriding toString. apcentral.collegeboard.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 10: Mutable Objects, and Chapter 11: Designing Classes. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Shadowing
- A local variable or parameter hiding a field of the same name, so the field is not what the name refers to.
- this
- A reference to the object the current instance method or constructor was called on.
- Static variable
- A variable declared static: one copy exists in the class rather than one per object.
- Static method
- A method belonging to the class, called through the class name, with no this and no access to instance fields.
- final
- A modifier allowing a variable to be assigned once. static final with a capitalised name is how a constant is written.
- Scope
- The region in which a declared name can be used: for a local variable, to the closing brace of its block.
- toString
- A method returning a String for the object. Java calls it whenever the object is joined to a String or printed.
Module 5: Arrays, ArrayList, and Grids
Four lessons on the containers that carry the second half of the exam. Arrays and the standard algorithms come first, traced on a real array of five test scores. Then ArrayList, its five methods, and the removal bug that skips every second element, shown failing and then fixed three different ways. Then the two-dimensional array and row-major traversal, and finally a grid problem taken from a written description all the way to running code.
Arrays and the standard algorithms
- Declare, create and fill an array, and read its length correctly.
- Implement the standard algorithms: maximum, sum, count, linear search, reverse and shift.
- Choose between an indexed loop and an enhanced for loop, and say what each one cannot do.
- Explain why passing an array to a method lets that method change your data.
Five test scores that will not fit in five variables
A class sits five tests and you want the highest mark, the average, and how many were above 85. With five separate int variables that is bearable. With thirty it is not, and with a number you do not know until the program runs it is impossible. An array is a single object holding a fixed number of values of the same type, each reachable by a number.
public class Basics {
public static void main(String[] args) {
int[] counts = new int[5];
System.out.println("length: " + counts.length);
System.out.println("counts[0] before anything: " + counts[0]);
counts[0] = 7;
counts[4] = 12;
System.out.println("counts[0] is now " + counts[0] + " and counts[4] is " + counts[4]);
System.out.print("all five: ");
for (int i = 0; i < counts.length; i++) {
System.out.print(counts[i] + " ");
}
System.out.println();
double[] rates = new double[3];
boolean[] flags = new boolean[2];
String[] names = new String[2];
System.out.println("defaults: " + rates[0] + ", " + flags[0] + ", " + names[0]);
int[] scores = {88, 91, 84, 96, 79};
System.out.println("declared with a list, length " + scores.length);
System.out.println("first " + scores[0] + ", last " + scores[scores.length - 1]);
System.out.println("about to ask for scores[5]");
System.out.println(scores[5]);
}
}
Its real output, ending as it was designed to:
length: 5
counts[0] before anything: 0
counts[0] is now 7 and counts[4] is 12
all five: 7 0 0 0 12
defaults: 0.0, false, null
declared with a list, length 5
first 88, last 79
about to ask for scores[5]
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at Basics.main(Basics.java:24)
Six facts are in that output, and they are all worth memorising.
new int[5] builds an array of five ints, all set to 0, because array elements get the same defaults as fields: 0, 0.0, false, null. Filling in two of them left the other three at 0, which is why the line printed 7 0 0 0 12 rather than just two values.
counts.length is the size. It is a field, not a method, so there are no brackets after it, and a String's length() is a method that does need them. Writing one for the other is the single commonest typing mistake in this material.
An array of Strings starts full of nulls. Not empty Strings, nulls. Calling names[0].length() on a fresh String array throws NullPointerException, and that trips up nearly everyone once.
int[] scores = {88, 91, 84, 96, 79}; creates and fills in one statement, and Java counts the length for you. This form works only in a declaration; you cannot write scores = {1, 2}; on a later line.
The last valid index is length - 1, so scores[scores.length - 1] is the idiom for the last element. And asking for scores[5] threw ArrayIndexOutOfBoundsException, with the length and the offending index both named in the message. This is the exception Lesson 6's off-by-one loop was heading for.
Remember: an array's size is fixed the moment it is created. You cannot grow it, and there is no method to add to it. That limitation is the whole reason ArrayList exists, which is the next lesson.
The algorithms the exam asks for, in one program
Unit 4 of the framework has a topic called Implementing Array Algorithms, and the list of algorithms is short and fixed. Here they are as methods, which is also how a free-response question will want them.
| Algorithm | The shape of it | What to watch |
|---|---|---|
| Maximum | assume the first, then beat it | start the loop at index 1, not 0 |
| Index of maximum | track an index, not a value | compare a[i] with a[best] |
| Sum and average | accumulator starting at 0 | cast before dividing or you get integer division |
| Count matching | accumulator starting at 0, with an if | count, do not sum |
| Linear search | return the index on a match | return -1 after the loop, never inside it |
| Reverse in place | swap i with length - 1 - i | stop at length / 2 or you undo your work |
| Shift | copy each element from its neighbour | save the element you overwrite first |
public class Standard {
public static int largest(int[] a) {
int best = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > best) {
best = a[i];
}
}
return best;
}
public static int indexOfLargest(int[] a) {
int bestIndex = 0;
for (int i = 1; i < a.length; i++) {
if (a[i] > a[bestIndex]) {
bestIndex = i;
}
}
return bestIndex;
}
public static int total(int[] a) {
int sum = 0;
for (int value : a) {
sum += value;
}
return sum;
}
public static int countAbove(int[] a, int limit) {
int count = 0;
for (int value : a) {
if (value > limit) {
count++;
}
}
return count;
}
public static int find(int[] a, int target) {
for (int i = 0; i < a.length; i++) {
if (a[i] == target) {
return i;
}
}
return -1;
}
public static void reverse(int[] a) {
for (int i = 0; i < a.length / 2; i++) {
int held = a[i];
a[i] = a[a.length - 1 - i];
a[a.length - 1 - i] = held;
}
}
public static void shiftLeft(int[] a) {
int first = a[0];
for (int i = 0; i < a.length - 1; i++) {
a[i] = a[i + 1];
}
a[a.length - 1] = first;
}
}
Run against the array 88, 91, 84, 96, 79 with a helper that prints an array in brackets, the whole program produced:
array: [88, 91, 84, 96, 79]
largest: 96
index of it: 3
total: 438
average: 87.6
how many over 85:3
find 84: 2
find 100: -1
after reverse: [79, 96, 84, 91, 88]
after shiftLeft: [96, 84, 91, 88, 79]
a single element reversed: [5]
still: [5]
Now go through the decisions, because each one is a place marks are lost.
Maximum. int best = a[0]; then loop from 1. Starting best at 0 instead is a bug that hides in test data: it gives the right answer for positive numbers and the wrong answer for an array of temperatures below freezing. Starting the loop at 0 rather than 1 merely wastes one comparison, which is harmless.
Index of maximum. The variable holds a position, so the comparison has to look up both values: a[i] > a[bestIndex]. It reported 3, which is where 96 sits. A question asking for the location rather than the value is testing exactly this distinction.
Average. total(scores) is 438 and the length is 5, and 438 divided by 5 in ints is 87, not 87.6. The program wrote (double) total(scores) / scores.length, which is the Lesson 2 fix in its natural habitat.
Linear search. find returns 2 for 84 and -1 for 100. The structure matters: return i; the moment you match, and return -1; only after the loop has finished. Putting the -1 inside an else means the method gives up on the first element that is not the target. This is linear search, it works on data in any order, and Lesson 18 shows what sorting buys you instead.
Reverse. Three lines swap two elements, and the third variable is not optional: without held, the first assignment destroys the value the second needs. The loop runs to a.length / 2, and integer division does the right thing for both even and odd lengths, leaving a middle element alone. The last two lines of output are a deliberate edge case: reversing an array of one element leaves it alone, because 1 / 2 is 0 and the loop runs zero times.
Shift. Every element moves down one and the displaced first element comes round to the end, so 79, 96, 84, 91, 88 became 96, 84, 91, 88, 79. The loop stops at a.length - 1 because the body reads a[i + 1], which is the same bound as the doubled-letters loop in Lesson 7. And first has to be saved before the loop starts, or it is overwritten on the very first step.
The enhanced for loop, and the one thing it cannot do
Three of those methods used a different loop: for (int value : a). This is the enhanced for loop, read as "for each int value in a". It gives you the elements without any index arithmetic, which removes every chance of an off-by-one error.
public class ForEach {
public static void main(String[] args) {
int[] values = {3, 1, 4, 1, 5};
System.out.print("with an index: ");
for (int i = 0; i < values.length; i++) {
System.out.print(values[i] + " ");
}
System.out.println();
System.out.print("with for-each: ");
for (int v : values) {
System.out.print(v + " ");
}
System.out.println();
for (int v : values) {
v = v * 100;
}
System.out.print("after assigning to the loop variable: ");
for (int v : values) {
System.out.print(v + " ");
}
System.out.println();
for (int i = 0; i < values.length; i++) {
values[i] = values[i] * 100;
}
System.out.print("after assigning through the index: ");
for (int v : values) {
System.out.print(v + " ");
}
System.out.println();
}
}
with an index: 3 1 4 1 5
with for-each: 3 1 4 1 5
after assigning to the loop variable: 3 1 4 1 5
after assigning through the index: 300 100 400 100 500
The third line is the lesson. A loop that multiplied v by 100 on every pass changed nothing at all, and the compiler raised no objection. v is a fresh local variable that receives a copy of each element, so assigning to it writes to the copy. The indexed loop, which assigns to values[i], really does change the array.
So the rule is: use the enhanced for loop when you are only reading, which covers sums, counts, searches for presence, and printing. Use an indexed loop when you need to write into the array, when you need the position of something, or when you need to look at two elements at once. Neither is more advanced than the other, and an exam question can require either.
An array is an object, and that changes what a method can do
Lesson 3 said a reference variable holds a way to reach an object. An array is an object, so everything from that lesson applies, and this time the object is mutable, which makes the consequences visible.
public class Shared {
public static void zeroOut(int[] a) {
for (int i = 0; i < a.length; i++) {
a[i] = 0;
}
}
public static void zeroOut(int n) {
n = 0;
}
public static void main(String[] args) {
int[] first = {2, 4, 6};
int[] second = first;
second[0] = 99;
System.out.println("first[0] after writing through second: " + first[0]);
System.out.println("are they the same object? " + (first == second));
int[] copy = new int[first.length];
for (int i = 0; i < first.length; i++) {
copy[i] = first[i];
}
copy[0] = -1;
System.out.println("after changing the copy, first[0] is " + first[0]);
System.out.println("are first and copy the same object? " + (first == copy));
int number = 42;
zeroOut(number);
System.out.println("the int after a method tried to zero it: " + number);
zeroOut(first);
System.out.println("the array after a method zeroed it: "
+ first[0] + " " + first[1] + " " + first[2]);
}
}
first[0] after writing through second: 99
are they the same object? true
after changing the copy, first[0] is 99
are first and copy the same object? false
the int after a method tried to zero it: 42
the array after a method zeroed it: 0 0 0
Line by line. int[] second = first; copied the reference, so writing through second changed what first sees, and first == second is true because there is one array with two names. Copying properly takes a loop and a new array, and after that first == copy is false even when the contents match, which is the String lesson again: == on any two references asks whether they are the same object.
The last two lines are the important pair. A method given an int gets a copy of the number, so zeroOut(number) left 42 alone. A method given an array gets a copy of the reference, which reaches the same array, so zeroOut(first) really did set every element to 0. Java always passes a copy of the value in the variable; for a primitive that is the number, and for an array that is the arrow. A method can therefore rearrange, refill or sort your array without returning anything, which is exactly how reverse and shiftLeft above are able to have return type void and still be useful.
Common misconceptions
- "An array's last index is its length." It is
length - 1. Asking forscores[5]in an array of five threw ArrayIndexOutOfBoundsException, with both numbers printed in the message. - "You write a.length() for an array." An array's length is a field with no brackets; a String's length is a method with brackets. Mixing them up is a compile error either way round.
- "A new String array is full of empty Strings." It is full of nulls, so calling a method on an element before assigning it throws NullPointerException.
- "You can grow an array by assigning past the end." The size is fixed at creation. There is no add method and no way to extend it; you build a bigger array and copy.
- "An enhanced for loop can change the array." It cannot. ForEach.java multiplied the loop variable by 100 five times and the array was untouched, with no warning from the compiler.
- "Starting the maximum at 0 is safe." Only if every value is positive. Start it at the first element, which always exists as long as the array is not empty.
- "A void method cannot change my array." It can change every element, because it holds a reference to the same array. zeroOut proved it.
Looking back
An array is a fixed-size object of same-typed elements, created with new int[n] or with a list in braces, indexed from 0 to length - 1, with length a field rather than a method. Unset elements hold 0, 0.0, false or null. The standard algorithms all reduce to a loop and an accumulator: seed the maximum with the first element, count with an if inside, search by returning the index on a match and -1 after the loop, reverse by swapping i with length - 1 - i up to the halfway point, and shift by saving the element you are about to overwrite. An enhanced for loop reads elements cleanly and cannot write to them. And because an array is an object, assigning it copies a reference and passing it to a method lets that method rewrite your data.
Worth holding on to: every one of those seven algorithms is a single loop. If you find yourself nesting loops for a maximum or a count, stop and look again.
Next lesson replaces the fixed size with something that grows. ArrayList has five methods on the exam reference sheet, one of which causes a bug so common it has a name, and you will watch it happen before you learn the three ways to avoid it.
Sources
- Oracle. (n.d.). The Java Tutorials: Arrays (an array is a container object of fixed length, the length field, declaration with an initialiser, and indexing from zero). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: The for Statement (the enhanced for statement and its use for iterating over collections and arrays). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topics 4.3 Array Creation and Access, 4.4 Array Traversals and 4.5 Implementing Array Algorithms. apcentral.collegeboard.org
- Wikipedia contributors. (2026). Linear search (scanning a list element by element until the target is found or the list is exhausted). en.wikipedia.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 7: Arrays and References. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Array
- A fixed-size object holding elements of one type, each reached by an index from 0 upwards.
- length
- An array's size, written as a field with no brackets. A String's length is a method, with brackets.
- ArrayIndexOutOfBoundsException
- Thrown at run time when an index is negative or at least the length.
- Enhanced for loop
- A loop of the form for (int v : a) that hands you each element in turn and cannot write to the array.
- Linear search
- Checking elements in order until the target is found, returning its index, or -1 after the loop ends.
- Swap
- Exchanging two elements using a third variable to hold the first value while the assignment happens.
- Shift
- Moving every element one position along, having first saved the element that will be overwritten.
- Pass by value
- Java copies what the variable holds. For a primitive that is the number; for an array it is the reference, so the method reaches your array.
ArrayList, and the bug that skips every second element
- Create and traverse an ArrayList using the five methods on the exam reference sheet.
- Explain why removing during a forward loop skips elements, and fix it three ways.
- Predict what an enhanced for loop does when the list is modified inside it.
- Store numbers in an ArrayList of Integer and account for the boxing that happens.
A list that should have lost every even number, and kept two
Here is a program with one job: take the numbers 4, 6, 8, 9, 10 and remove the even ones. The answer should be a list containing 9 alone. The program prints its own progress so you can watch what happens.
import java.util.ArrayList;
public class SkipBug {
public static void main(String[] args) {
ArrayList<Integer> nums = new ArrayList<Integer>();
nums.add(4);
nums.add(6);
nums.add(8);
nums.add(9);
nums.add(10);
System.out.println("start: " + nums);
for (int i = 0; i < nums.size(); i++) {
System.out.println("i = " + i + ", size = " + nums.size()
+ ", looking at " + nums.get(i));
if (nums.get(i) % 2 == 0) {
nums.remove(i);
System.out.println(" removed it, list is now " + nums);
}
}
System.out.println("finished with: " + nums);
System.out.println("every even number should have gone");
}
}
Its real output:
start: [4, 6, 8, 9, 10]
i = 0, size = 5, looking at 4
removed it, list is now [6, 8, 9, 10]
i = 1, size = 4, looking at 8
removed it, list is now [6, 9, 10]
i = 2, size = 3, looking at 10
removed it, list is now [6, 9]
finished with: [6, 9]
every even number should have gone
Six is still there. So is nothing else wrong: the program did not crash, did not warn, and removed three of the four even numbers. The tracing lines tell you precisely what went wrong, and it is worth reading them before the explanation.
The loop looked at index 0, found 4, removed it. Removing from an ArrayList closes the gap: everything after the removed element slides down one position, so 6 moved from index 1 to index 0. Then the loop did what loops do and incremented i to 1, which now holds 8. Six was never examined. The same thing happened again after 8 was removed, so 10 was reached but the 9 that slid into its place was skipped, and by luck 10 was even and did get removed.
| i | size | element at i | action | list after |
|---|---|---|---|---|
| 0 | 5 | 4 | removed | 6, 8, 9, 10 |
| 1 | 4 | 8 | removed (6 was skipped) | 6, 9, 10 |
| 2 | 3 | 10 | removed (9 was skipped) | 6, 9 |
| 3 | 2 | 3 is not less than 2, loop ends | 6, 9 |
This is the removal-while-iterating bug, and it is the most reliably examined bug in the whole course. Two things make it dangerous: it happens only when a removal is followed immediately by another element that also should be removed, so half your tests pass; and the loop's own condition i < nums.size() is re-evaluated every pass, so the loop also ends early, which hides the shortfall.
The point: when you remove an element, the index you are standing on now refers to a different element. Advancing past it is exactly wrong.
What an ArrayList is, and the five methods you are given
Before fixing it, the tour. An array's length is fixed at creation. An ArrayList grows and shrinks as you add and remove, which is why the exam's Question 3 is built around one. It lives in java.util, so a file that uses it starts with import java.util.ArrayList;, and it is declared with the type of its contents in angle brackets: ArrayList<String> is a list of Strings.
The Java Quick Reference lists exactly these methods, and they are all you need.
| Method | What it does | Returns |
|---|---|---|
int size() | how many elements there are | an int |
boolean add(E obj) | puts obj on the end | true, always |
void add(int i, E obj) | inserts at i, sliding the rest right | nothing |
E get(int i) | the element at i | that element |
E set(int i, E obj) | replaces the element at i | the element that was there |
E remove(int i) | deletes at i, sliding the rest left | the element removed |
import java.util.ArrayList;
public class Lists {
public static void main(String[] args) {
ArrayList<String> queue = new ArrayList<String>();
System.out.println("empty list: " + queue + ", size " + queue.size());
queue.add("Ada");
queue.add("Bao");
queue.add("Cleo");
System.out.println("after three adds: " + queue + ", size " + queue.size());
System.out.println("get(1) is " + queue.get(1));
queue.add(1, "Zed");
System.out.println("after add(1, Zed): " + queue);
String old = queue.set(0, "Amina");
System.out.println("set(0, Amina) returned " + old + " and gives " + queue);
String gone = queue.remove(2);
System.out.println("remove(2) returned " + gone + " and gives " + queue);
System.out.print("traversed with an index: ");
for (int i = 0; i < queue.size(); i++) {
System.out.print(queue.get(i) + " ");
}
System.out.println();
System.out.print("traversed with for-each: ");
for (String name : queue) {
System.out.print(name + " ");
}
System.out.println();
System.out.println("printed directly: " + queue);
}
}
empty list: [], size 0
after three adds: [Ada, Bao, Cleo], size 3
get(1) is Bao
after add(1, Zed): [Ada, Zed, Bao, Cleo]
set(0, Amina) returned Ada and gives [Amina, Zed, Bao, Cleo]
remove(2) returned Bao and gives [Amina, Zed, Cleo]
traversed with an index: Amina Zed Cleo
traversed with for-each: Amina Zed Cleo
printed directly: [Amina, Zed, Cleo]
Compare this list with an array and the differences are all in the notation. Size is size() with brackets, not length without them. An element is get(i), not [i]. Storing is set(i, value), not [i] = value. Mixing the two notations is the commonest compile error in this material, and the error message will say something like cannot find symbol: method length().
Two methods return something you might not expect. set gives back the element it replaced, which is how the program could print that Ada had been displaced. remove gives back the element it deleted, which printed Bao. You are free to ignore both return values, and usually will, but an exam question can ask what they evaluate to. And notice that printing an ArrayList directly gives a readable list in square brackets, unlike an array, which would print the at-sign form from Lesson 9.
One absence matters. The reference sheet has remove(int index) and nothing for removing by value. Real Java does let you remove an object by equality, but the exam works from the sheet, so when you need to delete "apple" from a list you find its index first and remove that.
Three ways to remove without skipping
Now the repair, three times over, each starting from the same list of 4, 6, 8, 9, 10.
ArrayList<Integer> a = build();
for (int i = a.size() - 1; i >= 0; i--) {
if (a.get(i) % 2 == 0) {
a.remove(i);
}
}
ArrayList<Integer> b = build();
int i = 0;
while (i < b.size()) {
if (b.get(i) % 2 == 0) {
b.remove(i);
} else {
i++;
}
}
ArrayList<Integer> c = build();
ArrayList<Integer> kept = new ArrayList<Integer>();
for (int value : c) {
if (value % 2 != 0) {
kept.add(value);
}
}
backwards loop: [9]
only advance if kept: [9]
built a new list: [9]
original untouched: [4, 6, 8, 9, 10]
All three give the right answer, and each is worth having.
Backwards. Run the index from size() - 1 down to 0. When an element is removed, everything that slides left is behind you, at indexes you have already dealt with, so nothing can be skipped. This is the shortest fix and the one to reach for by default.
Advance only when you keep. A while loop with the increment inside the else. If you removed, the next element has arrived at the same index, so you look at that index again. This is the fix that shows you have understood the bug rather than memorised a direction, and it is the one to use when you have to travel forwards for some other reason.
Build a new list. Do not remove at all. Walk the original with an enhanced for loop and copy the keepers into a fresh list. This is often the clearest code, and it has the side effect that the original survives, which the last line of output confirms. The cost is a second list in memory, which on an exam question is never the deciding factor.
The enhanced for loop turns the bug into an exception
One approach is not on that list, because it does not work at all.
import java.util.ArrayList;
public class Concurrent {
public static void main(String[] args) {
ArrayList<String> words = new ArrayList<String>();
words.add("keep");
words.add("drop");
words.add("keep");
words.add("drop");
System.out.println("start: " + words);
for (String w : words) {
System.out.println("looking at " + w);
if (w.equals("drop")) {
words.remove(1);
}
}
System.out.println("never reached: " + words);
}
}
The real output, with the last lines of the real stack trace:
start: [keep, drop, keep, drop]
looking at keep
looking at drop
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 Concurrent.main(Concurrent.java:11)
ConcurrentModificationException, thrown the moment the loop tried to fetch the next element after a removal. An enhanced for loop over an ArrayList keeps a record of how many structural changes the list has had; if that count changes underneath it, it refuses to continue rather than hand you a wrong answer. In one sense this is friendlier than the silent skipping: it fails loudly, and the trace names your line 11. The rule to take away is absolute. Never add to or remove from a list inside an enhanced for loop over that list. Reading is fine, and set is fine, because replacing an element is not a structural change.
An ArrayList holds objects, so numbers arrive wrapped
You cannot write ArrayList<int>. A list holds objects, and an int is not an object, which is what the Integer wrapper class from Lesson 4 is for. In practice the compiler does the conversion and you barely notice.
ArrayList<Integer> marks = new ArrayList<Integer>();
marks.add(88);
marks.add(91);
marks.add(84);
int total = 0;
for (int m : marks) {
total += m;
}
System.out.println("total " + total + ", mean " + (double) total / marks.size());
int first = marks.get(0);
marks.set(0, marks.get(0) + 10);
list: [88, 91, 84]
total 263, mean 87.66666666666667
get(0) unboxed into an int: 88
after adding ten to the first: [98, 91, 84]
largest: 98
index of 84: 2
index of 50: -1
marks.add(88) boxed the int into an Integer. for (int m : marks) unboxed each Integer back into an int. marks.set(0, marks.get(0) + 10) unboxed, added, and boxed again, all in one line. None of that needs writing out, and the exam expects you to use it freely.
The mean is the Lesson 2 trap in a new costume: total and size() are both ints, so the cast to double is what stops 263 divided by 3 from printing 87. And the search method that found 84 at index 2 compared with list.get(i) == target, where target is an int. That is safe, because with an int on one side Java unboxes the Integer and compares numbers. Comparing two Integer objects with == is the trap from Lesson 4, and it is still a trap.
Common misconceptions
- "Removing from a list leaves a gap." It closes the gap. Every later element slides one place towards the front and size drops by one, which is the whole cause of the skipping bug.
- "An ArrayList has a length field." It has
size(). Arrays havelength. Writinglist.lengthwill not compile. - "You get an element with square brackets." That is array notation. An ArrayList uses
get(i)to read andset(i, value)to write. - "Looping backwards is a trick with no reason behind it." The reason is that removal shifts only the elements after the one removed, and going backwards those are the ones you have already visited.
- "An enhanced for loop is the safe way to remove things." It throws ConcurrentModificationException. The safe ways are a backwards index, a while loop that advances only when it keeps, or building a new list.
- "You can declare ArrayList of int." You cannot. It is
ArrayList<Integer>, and autoboxing then hides the difference nearly everywhere. - "remove takes the value you want gone." On the exam reference sheet remove takes an index. Find the index first, then remove it.
Recap
An ArrayList is a growable list of objects, imported from java.util, declared with its element type in angle brackets, and worked with through six methods: size, two forms of add, get, set and remove. Removing closes the gap and drops the size, so a forward loop that removes and then increments skips the element that moved into the vacated place, which is why SkipBug finished with a 6 still in the list. The three cures are a backwards loop, a while loop that increments only in the else, and building a new list of the elements you want. An enhanced for loop over a list you modify throws ConcurrentModificationException rather than skipping. And because a list holds objects, ints become Integers automatically and become ints again automatically when you read them out.
In short: if a question shows you a loop that removes from a list while counting upwards, the answer is almost certainly that something got skipped. Trace it with a table of i, size and element, exactly as above.
Next lesson the container gains a second dimension. A two-dimensional array is an array whose elements are arrays, which sounds harder than it is, and the traversal that visits it in the right order has a name you should know before you meet it in an exam question.
Sources
- College Board. (2025). AP Computer Science A: 2026 Exam Reference Information, Java Quick Reference (the ArrayList methods available on the exam: size, add in both forms, get, set and remove by index, with the shifting behaviour of add and remove described). apcentral.collegeboard.org
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class ArrayList (a resizable-array implementation, and the fail-fast iterator that throws ConcurrentModificationException when the list is structurally modified during iteration). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topics 4.7 Wrapper Classes, 4.8 ArrayList Methods, 4.9 ArrayList Traversals and 4.10 Implementing ArrayList Algorithms. apcentral.collegeboard.org
- Wikipedia contributors. (2026). Dynamic array (a list that grows and shrinks, and the cost of inserting or deleting in the middle because later elements must move). en.wikipedia.org
- Key terms
- ArrayList
- A growable list of objects from java.util, declared with its element type in angle brackets.
- size()
- The number of elements currently in a list. Arrays use the field length instead.
- get and set
- Reading and replacing an element by index. A list has no square-bracket notation.
- Structural modification
- An add or a remove, which changes the size. A set is not one.
- Removal-while-iterating bug
- Skipping elements because a removal slides later elements down while the index still advances.
- ConcurrentModificationException
- Thrown when a list is structurally modified during an enhanced for loop over it.
- Boxing in a list
- An int added to an ArrayList of Integer becomes an Integer, and becomes an int again when read out.
Two-dimensional arrays and row-major traversal
- Declare and create a two-dimensional array and read both of its lengths.
- Traverse a grid in row-major and in column-major order and say which loop is outermost.
- Total a row, total a column, and find the position of the largest cell.
- Use a nested enhanced for loop over a grid, and say when it will not do.
Three rows of four seats
A small cinema has three rows with four seats in each. You could keep three arrays of four, named row0, row1 and row2, and then discover that no loop can visit all twelve seats, because a loop can count but it cannot pick a different variable name each time. A two-dimensional array solves it: one object, two indexes, first the row and then the column.
public class Grid2 {
public static void main(String[] args) {
int[][] hall = new int[3][4];
System.out.println("rows: " + hall.length);
System.out.println("columns in row 0: " + hall[0].length);
System.out.println("hall[0][0] before anything: " + hall[0][0]);
hall[1][2] = 5;
hall[2][3] = 9;
for (int r = 0; r < hall.length; r++) {
for (int c = 0; c < hall[r].length; c++) {
System.out.print(hall[r][c] + " ");
}
System.out.println();
}
System.out.println("---");
int[][] board = {{1, 2, 3}, {4, 5, 6}};
System.out.println("board has " + board.length + " rows and "
+ board[0].length + " columns");
System.out.println("board[1][0] is " + board[1][0]);
System.out.println("row 1 as an array object: " + (board[1].length) + " long");
System.out.println("about to ask for board[2][0]");
System.out.println(board[2][0]);
}
}
Its real output, ending in the failure it was written to cause:
rows: 3
columns in row 0: 4
hall[0][0] before anything: 0
0 0 0 0
0 0 5 0
0 0 0 9
---
board has 2 rows and 3 columns
board[1][0] is 4
row 1 as an array object: 3 long
about to ask for board[2][0]
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
at Grid2.main(Grid2.java:22)
new int[3][4] means three rows of four, in that order, all twelve elements starting at 0. The nested loop that printed it is the one from Lesson 7 with an array lookup in the middle, and the empty println sits inside the outer loop so each row gets a line. The brace form works too: {{1, 2, 3}, {4, 5, 6}} is two rows of three, with the inner braces marking the rows.
The exception is worth reading. Asking for board[2][0] on a two-row grid reported that index 2 was out of bounds for length 2. Note which length it means: the number of rows. A row index that is too big and a column index that is too big give the same kind of exception, and telling them apart means checking which number the message names.
Why there are two lengths, and what they mean
A two-dimensional array in Java is really an array whose elements are themselves arrays. That single sentence explains all the notation.
| Expression | Meaning | On the 3 by 4 hall |
|---|---|---|
hall | the whole grid | an array of 3 arrays |
hall.length | the number of rows | 3 |
hall[1] | row 1, itself an array | an array of 4 ints |
hall[1].length | the number of columns in row 1 | 4 |
hall[0].length | the usual way to ask for the width | 4 |
hall[1][2] | the cell in row 1, column 2 | one int |
So hall.length is the height and hall[0].length is the width. Getting those the wrong way round is the most common mistake in this topic, and the symptom is an ArrayIndexOutOfBoundsException on a grid that is not square, while a square grid hides the error completely. Test your grid code on something that is not square.
Because each row is a separate array object, Java allows rows of different lengths. The College Board's framework rules that out: non-rectangular grids are outside the scope of the exam, so every grid you meet has rows of equal length. That is why hall[0].length is a safe way to ask for the width. Writing hall[r].length in the inner loop, as the program above does, is still the better habit and costs nothing.
Key idea: the first index chooses a row, the second chooses a position within that row. Say it as "row then column" every time you write a pair of brackets.
Row-major and column-major, side by side
Two nested loops can visit a grid in two different orders, depending on which one is on the outside. The names come from which index changes more slowly.
public class Major {
public static void main(String[] args) {
int[][] grid = {{10, 20, 30, 40}, {50, 60, 70, 80}, {90, 100, 110, 120}};
System.out.print("row-major order: ");
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[r].length; c++) {
System.out.print(grid[r][c] + " ");
}
}
System.out.println();
System.out.print("column-major order: ");
for (int c = 0; c < grid[0].length; c++) {
for (int r = 0; r < grid.length; r++) {
System.out.print(grid[r][c] + " ");
}
}
System.out.println();
System.out.print("nested for-each: ");
for (int[] row : grid) {
for (int value : row) {
System.out.print(value + " ");
}
}
System.out.println();
System.out.print("visit order as coordinates: ");
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[r].length; c++) {
System.out.print("(" + r + "," + c + ") ");
}
}
System.out.println();
}
}
row-major order: 10 20 30 40 50 60 70 80 90 100 110 120
column-major order: 10 50 90 20 60 100 30 70 110 40 80 120
nested for-each: 10 20 30 40 50 60 70 80 90 100 110 120
visit order as coordinates: (0,0) (0,1) (0,2) (0,3) (1,0) (1,1) (1,2) (1,3) (2,0) (2,1) (2,2) (2,3)
| Row-major | Column-major | |
|---|---|---|
| Outer loop counts | rows | columns |
| Outer bound | grid.length | grid[0].length |
| Inner bound | grid[r].length | grid.length |
| Order of visits | 10 20 30 40 then 50 60 70 80 | 10 50 90 then 20 60 100 |
| Reads like | a page of text | a column of a spreadsheet |
| Enhanced for works? | yes | no |
Row-major order is the default, and it is the order an exam question means when it says traverse the array. It is also the only order a nested enhanced for loop can produce, and the reason is exactly the array-of-arrays picture: for (int[] row : grid) hands you whole rows, so you get the elements row by row and there is no way to ask for a column. When a problem is about columns, you need indexes.
The last line of output is the one to photograph in your memory. Row-major visits (0,0), (0,1), (0,2), (0,3), then (1,0). The column index runs all the way round before the row index moves at all, because the inner loop restarts on every pass of the outer, as Lesson 7 established.
Totals down the rows and across the columns
Almost every grid question is a totalling or counting question, and each one is a single loop or a nested pair. Here they are as methods on a grid of weekly sales figures, three weeks by four products.
public static int rowTotal(int[][] g, int r) {
int sum = 0;
for (int c = 0; c < g[r].length; c++) {
sum += g[r][c];
}
return sum;
}
public static int columnTotal(int[][] g, int c) {
int sum = 0;
for (int r = 0; r < g.length; r++) {
sum += g[r][c];
}
return sum;
}
public static int grandTotal(int[][] g) {
int sum = 0;
for (int[] row : g) {
for (int value : row) {
sum += value;
}
}
return sum;
}
public static int countAbove(int[][] g, int limit) {
int count = 0;
for (int[] row : g) {
for (int value : row) {
if (value > limit) {
count++;
}
}
}
return count;
}
Run on the grid 12 7 19 4 / 8 22 3 15 / 17 9 11 6, together with a nested loop that tracks the position of the largest cell, the program printed:
row 0 total 42
row 1 total 48
row 2 total 43
column 0 total 37
column 1 total 38
column 2 total 33
column 3 total 25
grand total 133
cells above 10: 6
largest cell 22 at row 1, column 1
Look at the two single-loop methods. rowTotal is handed a row number and loops over columns, so the row index is fixed and the column index moves. columnTotal is the mirror image: the column is fixed and the row index moves, which is why its loop bound is g.length. If you can write those two without hesitating over which bound goes where, you have this topic.
The row totals add to 133 and so do the column totals, which is a free check on your work: whichever way you slice a grid, the grand total is the same. If your two sets of totals disagree, one of your loops has its bounds crossed over.
Finding the largest cell needs two variables, not one, because a position in a grid is a pair. The loop keeps bestRow and bestColumn and compares sales[r][c] with sales[bestRow][bestColumn], which is the index-of-maximum pattern from Lesson 10 with both coordinates carried along. Reporting only the value would lose the information the question probably wanted.
Common misconceptions
- "grid.length is the number of cells." It is the number of rows. The cell count is
grid.length * grid[0].length, which for the 3 by 4 hall is 12. - "grid[r][c] means column r, row c." It is row first, then column. On a square grid the mistake is invisible; on a 3 by 4 grid it throws.
- "A nested enhanced for loop can give me the columns." It cannot. It hands you whole rows in order, so it produces row-major order only. Column work needs indexes.
- "An enhanced for loop over a grid lets me fill the cells." Reading works, writing does not, for the same reason as in Lesson 10: the loop variable is a copy. Note though that
rowinfor (int[] row : grid)is a reference to a real row array, sorow[0] = 5does change the grid, whilerow = new int[4]does not. - "Rows can be different lengths, so I always need grid[r].length." Java allows ragged grids but the exam does not use them. Use
grid[r].lengthanyway; it is never wrong and it costs nothing. - "To find the biggest cell I only need to remember the value." If the question asks where it is, you need both indexes, updated together in the same if.
The short version
A two-dimensional array is an array of arrays, created with new int[rows][columns] or with nested braces, and indexed row first then column. grid.length is the number of rows and grid[0].length the number of columns, and mixing them up is invisible on a square grid and fatal on any other. Row-major traversal puts the row loop outside, is the default meaning of traversing a grid, and is the only order a nested enhanced for loop can produce; column-major puts the column loop outside and needs indexes. Totalling a row fixes the row and loops over columns, totalling a column does the opposite, and the two sets of totals must agree. Finding the largest cell means tracking two indexes together.
The core of it: write for (int r ...) then for (int c ...) then grid[r][c], in that order, until it is automatic. Nearly every grid question is that skeleton with something different in the middle.
Next lesson does not introduce anything new. It takes a grid problem from a written description all the way to running code, including the part everyone finds hardest: looking at a cell's neighbours without falling off the edge of the board.
Sources
- Oracle. (n.d.). The Java Tutorials: Arrays (a multidimensional array is an array whose components are themselves arrays, so rows may differ in length). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topics 4.11 2D Array Creation and Access, 4.12 2D Array Traversals and 4.13 Implementing 2D Array Algorithms, including the exclusion statement that non-rectangular 2D arrays are outside the scope of the course and exam. apcentral.collegeboard.org
- Wikipedia contributors. (2026). Row- and column-major order (the two orders in which a rectangular grid can be laid out and traversed). en.wikipedia.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 15: Arrays of Arrays. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Two-dimensional array
- An array whose elements are arrays, indexed by row and then by column.
- grid.length
- The number of rows in a two-dimensional array.
- grid[0].length
- The number of columns, being the length of the first row.
- Row-major traversal
- Visiting every cell with the row loop outermost, so a whole row is finished before the next begins.
- Column-major traversal
- Visiting with the column loop outermost, which an enhanced for loop cannot produce.
- Rectangular grid
- A grid whose rows all have the same length. The exam uses only these.
- Cell position
- A pair of indexes. Recording where something is in a grid needs two variables, not one.
A grid problem worked end to end
- Turn a written grid specification into method headers before writing any code.
- Visit the eight neighbours of a cell with a nested loop whose bounds are relative to that cell.
- Guard every grid lookup with a bounds check, and say why the order of the tests matters.
- Build a new grid from an old one so every cell is judged from the same generation.
Five mines and twenty-five answers
A 5 by 5 minefield has mines at row 0 column 1, row 1 column 3, row 3 column 0, row 3 column 4 and row 4 column 2. For each of the twenty-five squares, print how many of the squares touching it hold a mine. That is the whole problem.
No new language feature appears in this lesson. Everything it needs came from Lesson 12 or earlier. What is new is the distance between a paragraph of English and a program that runs, and the one obstacle that stops nearly everybody the first time: a square in the middle of the board has eight neighbours, a square in a corner has three, and asking Java for a neighbour that is not there throws.
Turning the paragraph into method headers
Decide what the pieces are, and what each one hands back, before you write a line. A free-response question does this for you by supplying the headers. When nobody supplies them you write them yourself, and it is worth building the habit now.
| Method | Given | Hands back |
|---|---|---|
inBounds(field, r, c) | the grid and a pair of numbers | true when row r and column c are real positions in that grid |
countNeighbours(field, row, col) | the grid and one square | how many of that square's neighbours hold a mine |
countAll(field) | the grid | a new grid of the same size holding every count |
printCounts(counts) | a grid of ints | nothing; it prints |
Three decisions in that table are worth saying out loud. The mines are a boolean[][], because a square either holds a mine or does not, and a boolean is exactly that much information. The counts live in a separate int[][] rather than being written back over the mines, because they are different information about the same squares. And countAll returns a grid instead of printing one, so the counting can be tested without the printing getting in the way.
Notice also what inBounds buys. The dangerous part of this problem is asking for a position that does not exist, and that danger now has a name, a return type, and one place where it is decided. When something goes wrong later you have one four-line method to check rather than a condition buried in a nested loop.
The loop that reaches every neighbour
A square at row 2 column 3 has neighbours at rows 1, 2 and 3 and at columns 2, 3 and 4. That is nine positions in all, one of which is the square itself. Written as a loop, the bounds are relative to the square rather than to the grid:
for (int r = row - 1; r <= row + 1; r++) {
for (int c = col - 1; c <= col + 1; c++) {
...
}
}
Look at the condition. Every loop so far in this course counted up to but not including a limit, because array indexes run from 0 to length minus 1. Here row + 1 is not a limit to stop before; it is a position you actually want to visit. So the condition is <=, and the loop runs three times. Three values of r times three values of c gives nine positions, and the eight that matter are all of them except the one where r equals row and c equals col.
That set of nine has a name. In cellular automata it is the Moore neighbourhood: a central cell and the eight cells that surround it. You will meet the same nine-cell loop in minesweeper, in image blurring, and in the Game of Life at the end of this lesson.
The version that crashes, and the message it leaves behind
Here is the neighbour loop written with no protection at all, on a smaller 3 by 3 field with mines at row 0 column 1 and row 2 column 0. It asks for the middle square first, then for the top left corner.
public class Naive {
public static void main(String[] args) {
boolean[][] mines = {
{false, true, false},
{false, false, false},
{true, false, false}
};
System.out.println("count at the middle, row 1 column 1: " + countNeighbours(mines, 1, 1));
System.out.println("now the top left corner, row 0 column 0:");
System.out.println(countNeighbours(mines, 0, 0));
}
public static int countNeighbours(boolean[][] field, int row, int col) {
int found = 0;
for (int r = row - 1; r <= row + 1; r++) {
for (int c = col - 1; c <= col + 1; c++) {
if (!(r == row && c == col) && field[r][c]) {
found++;
}
}
}
return found;
}
}
Its real output:
count at the middle, row 1 column 1: 2
now the top left corner, row 0 column 0:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
at Naive.countNeighbours(Naive.java:17)
at Naive.main(Naive.java:10)
The middle square worked. It has all eight neighbours, two of which hold mines, so 2 is right. Then the corner killed the program, and the message says precisely what happened: Index -1 out of bounds for length 3. For the square at row 0 column 0, the loop starts at row - 1, which is -1. There is no row above the top row. Java does not quietly return false for a square that is not there; it stops the program.
Read the two indented lines from the bottom up, because that is the order the calls were made in. main at line 10 called countNeighbours, and line 17 inside countNeighbours is where the lookup failed. Lesson 21 takes that skill apart properly, but even now those two lines tell you which line of which method to go and look at.
What matters here: the edge of a grid is not an unusual case you can deal with later. It is a quarter of the squares on a 5 by 5 board and all four corners, and it is the difference between a program that runs and a program that stops.
inBounds, and why the order of the tests is not optional
The fix is one small method that answers one question:
public static boolean inBounds(boolean[][] field, int r, int c) {
return r >= 0 && r < field.length && c >= 0 && c < field[0].length;
}
Four conditions joined with and: the row is not above the top, not below the bottom, and the column is not left of nor right of the board. Notice that it returns the boolean expression directly rather than using an if statement with return true and return false, which is the tidier of the two forms you met in Lesson 5.
Now the guarded test inside the neighbour loop:
if (inBounds(field, r, c) && !(r == row && c == col) && field[r][c]) {
found++;
}
The order of those three tests is load-bearing. Oracle's tutorial on the conditional operators says it in one sentence: these operators evaluate the second operand only if needed. So if inBounds(field, r, c) is false, Java never evaluates field[r][c], and the lookup that would have thrown is simply not performed. Write the same three tests in the other order, with field[r][c] first, and the program throws at the first corner exactly as Naive did. Short-circuit evaluation from Lesson 5 is not a curiosity here; it is the whole mechanism by which the guard works.
The middle test, !(r == row && c == col), skips the square itself: a square is not its own neighbour. By De Morgan's laws from the same lesson you could write it as (r != row || c != col), and it would behave identically. Choose whichever you can read at speed under exam pressure, and then always write that one.
The finished program, and what it printed
public class Minefield {
public static void main(String[] args) {
boolean[][] mines = {
{false, true, false, false, false},
{false, false, false, true, false},
{false, false, false, false, false},
{true, false, false, false, true},
{false, false, true, false, false}
};
System.out.println("the field, * for a mine and . for empty:");
printField(mines);
int[][] counts = countAll(mines);
System.out.println("neighbour counts:");
printCounts(counts);
System.out.println("corner counts, clockwise from top left: "
+ counts[0][0] + " " + counts[0][4] + " "
+ counts[4][4] + " " + counts[4][0]);
System.out.println("mines on the board: " + countMines(mines));
System.out.println("sum of every count: " + sumAll(counts));
}
public static boolean inBounds(boolean[][] field, int r, int c) {
return r >= 0 && r < field.length && c >= 0 && c < field[0].length;
}
public static int countNeighbours(boolean[][] field, int row, int col) {
int found = 0;
for (int r = row - 1; r <= row + 1; r++) {
for (int c = col - 1; c <= col + 1; c++) {
if (inBounds(field, r, c) && !(r == row && c == col) && field[r][c]) {
found++;
}
}
}
return found;
}
public static int[][] countAll(boolean[][] field) {
int[][] counts = new int[field.length][field[0].length];
for (int r = 0; r < field.length; r++) {
for (int c = 0; c < field[r].length; c++) {
counts[r][c] = countNeighbours(field, r, c);
}
}
return counts;
}
public static int countMines(boolean[][] field) {
int total = 0;
for (boolean[] row : field) {
for (boolean cell : row) {
if (cell) {
total++;
}
}
}
return total;
}
public static int sumAll(int[][] counts) {
int total = 0;
for (int[] row : counts) {
for (int value : row) {
total += value;
}
}
return total;
}
public static void printField(boolean[][] field) {
for (boolean[] row : field) {
String line = " ";
for (boolean cell : row) {
if (cell) {
line += "* ";
} else {
line += ". ";
}
}
System.out.println(line);
}
}
public static void printCounts(int[][] counts) {
for (int[] row : counts) {
String line = " ";
for (int value : row) {
line += value + " ";
}
System.out.println(line);
}
}
}
Its real output:
the field, * for a mine and . for empty:
. * . . .
. . . * .
. . . . .
* . . . *
. . * . .
neighbour counts:
1 0 2 1 1
1 1 2 0 1
1 1 1 2 2
0 2 1 2 0
1 2 0 2 1
corner counts, clockwise from top left: 1 1 1 1
mines on the board: 5
sum of every count: 28
Check a few cells against the field by hand, because that is the only way to believe a grid program. The square at row 0 column 0 touches row 0 column 1, row 1 column 0 and row 1 column 1. One of those three holds a mine, so 1 is right. The square at row 0 column 1 holds a mine itself, and none of its five neighbours does, so its count is 0: the program reports how many mines are around a square, and says nothing about the square itself. That is what the specification asked for, and it is the sort of detail a question states in one clause that is easy to miss.
Two structural points about countAll. It creates new int[field.length][field[0].length], so the answer grid is the same shape as the board whatever shape the board is; nothing in the method mentions 5. And its inner bound is field[r].length, the width of the row being visited, which is the habit from Lesson 12 that costs nothing and is never wrong.
A check that costs nothing
The last line printed 28. That number is not decoration; it is a test. Every mine contributes exactly 1 to each of its own in-bounds neighbours, so the sum of all twenty-five counts must equal the total number of neighbours the mines have. The mine at row 1 column 3 sits away from every edge and so has 8 neighbours. The other four sit on an edge and have 5 each. 8 plus 5 plus 5 plus 5 plus 5 is 28, and 28 is what the program printed.
That kind of arithmetic check is worth more than staring at the code. If the guard were wrong in the direction of skipping legal squares, the sum would come out too low; if the middle test were missing, so that each mine counted itself, the sum would be 33. You can often invent such a check for a grid problem in under a minute, and it catches the errors that reading does not.
The upshot: when a grid program prints a grid, find one number you can work out on paper and compare it. A whole-board total is usually the easiest one to construct.
Common misconceptions
- "I can handle the edges with four special cases: top, bottom, left, right." You can, and it will take thirty lines and still be wrong at the corners, which are two special cases at once. One
inBoundstest handles all of it, including the corners, because it asks about a single position rather than about a region. - "I will check the bounds afterwards." There is no afterwards.
field[-1][0]throws at the moment it is evaluated, so the check has to happen before the lookup, which is what short-circuit and forces. - "The neighbour loops should run from 0 to length." Those are the bounds for visiting the whole grid, not for visiting one square's surroundings. Neighbour bounds are relative to the square:
row - 1torow + 1. - "A square counts itself if it holds a mine." Only if you let it. The
!(r == row && c == col)test is the whole reason the mine at row 0 column 1 printed a count of 0 rather than 1. - "A new generation can be written into the grid I am reading." This is the one that quietly produces plausible wrong answers in the Game of Life. Changing a cell before its neighbour has been judged means the neighbour is judged partly on the new generation, so the result depends on the order of your loops. Build a new grid and return it.
Putting it together
A grid problem is nearly always the same four moves. Read the specification and write the method headers, including a helper for whatever is dangerous. Visit every cell with the row loop outside and the column loop inside. Visit one cell's neighbours with a second nested loop whose bounds run from row - 1 to row + 1 inclusive, guarded by inBounds placed first so short-circuit and protects the lookup. Then find one number you can check by hand.
Worth holding on to: inBounds is the single most reusable method in this part of the course. Write it once, from memory, in every grid question you ever answer, and put it first in every condition that indexes the grid.
That closes Module 5 and the first half of the course. You can now write a class, fill an array, grow an ArrayList and walk a grid. Module 6 asks a different question: what happens when one class is a special kind of another, and how Java decides at run time which version of a method to call.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topics 4.11 to 4.13 on 2D array creation, traversal and algorithms, and Exam Information, Section II, free-response Question 4: 2D Array, worth 6 points. apcentral.collegeboard.org
- Oracle. (n.d.). The Java Tutorials: Equality, Relational, and Conditional Operators (the conditional operators show short-circuiting behaviour, so the second operand is evaluated only if needed). docs.oracle.com
- Wikipedia contributors. (2026). Moore neighborhood (a central cell together with the eight cells that surround it, on a two-dimensional square lattice). en.wikipedia.org
- Wikipedia contributors. (2026). Conway's Game of Life (a live cell with two or three live neighbours survives, a dead cell with exactly three live neighbours becomes alive, and each cell is judged on the neighbours' old states rather than their new ones). en.wikipedia.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 15: Arrays of Arrays. O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Neighbour
- One of the eight squares touching a given square, counting the diagonals.
- Moore neighbourhood
- A cell together with the eight cells surrounding it: the nine positions a neighbour loop visits.
- Bounds check
- A test that a row and column are real positions in the grid, made before the grid is indexed.
- Guard
- A condition arranged so that a dangerous operation runs only when it is safe to run.
- Helper method
- A small method written to be called by another, such as inBounds, so the main logic stays readable.
- Generation
- One complete update of a grid in which every cell is judged from the previous state.
- Edge case
- An input at the boundary of what is allowed, such as a corner square, where working code often fails.
Module 6: Inheritance and Polymorphism
Three lessons on what happens when one class is a special kind of another. First what extends actually gives a subclass, with three real compiler errors showing what it does not give. Then overriding and dynamic binding: the same call, written once, running different code depending on the object it lands on. Then abstract classes and interfaces, with an honest account of how much of this the current exam asks you to write and how much it asks you only to read.
extends and super: what a subclass really inherits
- Write a subclass with extends and pass values to the superclass constructor with super.
- Say which members a subclass inherits, and read the compiler errors that appear when it reaches for one it does not.
- Explain why a superclass constructor body finishes before the subclass constructor body starts.
- Store objects of two subclasses in one array of the superclass type and traverse it.
A toll bridge, two kinds of vehicle, one set of rules
A toll bridge charges 2.50 for every axle. A car has two axles and pays 5.00. A three-axle truck pays 7.50. A car with a prepaid tag gets 1.00 off, and a truck carrying more than four tonnes per axle is flagged as overweight. Write all of that as one class and you will end up passing a boolean called isTruck into half the methods, with an if statement at the top of each to decide which half of the class is in charge.
The alternative is to say what is actually true: a car and a truck are both vehicles. They share a number plate, an axle count and a toll. Each then has something the other does not. Java lets you write the shared part once and the differences separately, and the keyword that connects them is extends.
public class Vehicle {
private String plate;
private int axles;
public Vehicle(String plate, int axles) {
this.plate = plate;
this.axles = axles;
System.out.println(" Vehicle constructor ran for " + plate);
}
public String getPlate() {
return plate;
}
public int getAxles() {
return axles;
}
public double toll() {
return 2.5 * axles;
}
public String describe() {
return plate + ", " + axles + " axles, toll " + toll();
}
}
That is an ordinary class of the kind you wrote in Module 4: two private fields, a constructor, two accessors and two methods. The println in the constructor is there only so you can watch the order of events in a moment; a real class would not print from a constructor.
public class Car extends Vehicle {
private boolean hasTag;
public Car(String plate, boolean hasTag) {
super(plate, 2);
this.hasTag = hasTag;
System.out.println(" Car constructor ran for " + plate);
}
public boolean getHasTag() {
return hasTag;
}
public double discountedToll() {
if (hasTag) {
return toll() - 1.0;
}
return toll();
}
}
Read the first line as a claim about the world: a Car is a Vehicle. The word for that claim is the is-a relationship, and it is the test for whether inheritance is the right tool. A car is a vehicle, so Car extends Vehicle is honest. A car has an engine, but a car is not an engine, so Car extends Engine would be nonsense even though it would compile.
Car declares one field of its own and one method of its own. It never declares plate, axles, getPlate, getAxles, toll or describe, and yet a Car object has all of them. Oracle's tutorial puts it plainly: classes derived from other classes inherit the fields and methods of those classes.
The constructor that runs before yours
Look at the first line of the Car constructor. super(plate, 2) calls the Vehicle constructor, handing it the plate it was given and the number 2, because every car has two axles and there is no point asking. Here is Truck, which does ask:
public class Truck extends Vehicle {
private int tonnes;
public Truck(String plate, int axles, int tonnes) {
super(plate, axles);
this.tonnes = tonnes;
System.out.println(" Truck constructor ran for " + plate);
}
public int getTonnes() {
return tonnes;
}
public boolean overweight() {
return tonnes > 4 * getAxles();
}
}
Now a program that builds one of each and uses them:
public class Main {
public static void main(String[] args) {
System.out.println("building a Car:");
Car c = new Car("KP 41 TDE", true);
System.out.println("building a Truck:");
Truck t = new Truck("LM 07 RRX", 3, 14);
System.out.println();
System.out.println("plate from the Car: " + c.getPlate());
System.out.println("axles from the Car: " + c.getAxles());
System.out.println("toll from the Car: " + c.toll());
System.out.println("describe on the Car: " + c.describe());
System.out.println("discounted toll, a Car method: " + c.discountedToll());
System.out.println();
System.out.println("describe on the Truck: " + t.describe());
System.out.println("tonnes: " + t.getTonnes() + ", overweight? " + t.overweight());
System.out.println();
System.out.println("a Vehicle variable holding a Car:");
Vehicle v = new Car("ZZ 99 AAA", false);
System.out.println("describe through v: " + v.describe());
System.out.println();
System.out.println("one array, both kinds of vehicle:");
Vehicle[] fleet = {c, t, v};
double takings = 0.0;
for (Vehicle each : fleet) {
System.out.println(" " + each.describe());
takings += each.toll();
}
System.out.println("takings for the day: " + takings);
}
}
Four files, so this one is compiled with javac on all of them and then run. Its real output:
building a Car:
Vehicle constructor ran for KP 41 TDE
Car constructor ran for KP 41 TDE
building a Truck:
Vehicle constructor ran for LM 07 RRX
Truck constructor ran for LM 07 RRX
plate from the Car: KP 41 TDE
axles from the Car: 2
toll from the Car: 5.0
describe on the Car: KP 41 TDE, 2 axles, toll 5.0
discounted toll, a Car method: 4.0
describe on the Truck: LM 07 RRX, 3 axles, toll 7.5
tonnes: 14, overweight? true
a Vehicle variable holding a Car:
Vehicle constructor ran for ZZ 99 AAA
Car constructor ran for ZZ 99 AAA
describe through v: ZZ 99 AAA, 2 axles, toll 5.0
one array, both kinds of vehicle:
KP 41 TDE, 2 axles, toll 5.0
LM 07 RRX, 3 axles, toll 7.5
ZZ 99 AAA, 2 axles, toll 5.0
takings for the day: 17.5
The first four lines are the point of the printlns. Building one Car produced two lines of output, in the order Vehicle then Car, because super(plate, 2) is the first statement of the Car constructor: the superclass part of the object is set up completely before the subclass constructor does anything else. Building a Truck did the same. Every object of a subclass is built from the top of the hierarchy down.
describe() is worth a second look. It lives in Vehicle, it was never rewritten, and yet it printed 2 axles for the car and 3 for the truck, because it reads the object's own field. One method, written once, telling the truth about both kinds of vehicle.
Why this matters: a subclass object is not a separate little object glued to a superclass object. It is one object that has every field and every method of both, built in one pass from the superclass downwards.
Three errors the compiler will hand you
What a subclass does not get is best learned from the compiler, so here are three programs that fail, with the messages they actually produce.
First. A method in Car that reaches for the plate directly:
public String shortLabel() {
return plate + " (car)";
}
Car.java:10: error: plate has private access in Vehicle
return plate + " (car)";
^
1 error
The field is there in the object. It is not reachable by name, because private means private to the class that declared it, and a subclass is a different class. This is the rule from Lesson 8 applying exactly as written, and the fix is the accessor: getPlate() + " (car)". It is why Truck.overweight above calls getAxles() rather than reading axles.
Second. Storing a Car in a Vehicle variable and then asking it for a Car method:
Vehicle v = new Car("ZZ 99 AAA", true);
System.out.println(v.describe());
System.out.println(v.discountedToll());
Main.java:5: error: cannot find symbol
System.out.println(v.discountedToll());
^
symbol: method discountedToll()
location: variable v of type Vehicle
1 error
Read the location line: variable v of type Vehicle. The object on the heap really is a Car, but the compiler is not looking at the object; it is looking at the declared type of the variable. A Vehicle variable may be asked for anything Vehicle promises and nothing more. That distinction between the declared type and the actual object is the whole subject of Lesson 15, and this error message is the first place you meet it.
Third. A subclass constructor that forgets super altogether:
public class Motorcycle extends Vehicle {
private boolean sidecar;
public Motorcycle(String plate, boolean sidecar) {
this.sidecar = sidecar;
}
}
Motorcycle.java:4: error: constructor Vehicle in class Vehicle cannot be applied to given types;
public Motorcycle(String plate, boolean sidecar) {
^
required: String,int
found: no arguments
reason: actual and formal argument lists differ in length
1 error
Nothing in that constructor mentions Vehicle, so where did the call come from? The compiler put it there. Oracle's page on the keyword super states the rule: if a constructor does not explicitly invoke a superclass constructor, the compiler inserts a call to the no-argument constructor of the superclass, and if the superclass has no no-argument constructor you get a compile-time error. Vehicle requires a String and an int, the inserted call supplies neither, and the message even tells you what was required and what was found. The fix is to write super(plate, 2) as the first statement.
That word first is a rule too: a super call must be the first statement in the constructor. Java will not let you half-build an object and then initialise its superclass part afterwards.
What is inherited, and what is not
| Member of the superclass | Does the subclass get it? |
|---|---|
public methods, such as getPlate and toll | Yes, and they can be called as though written in the subclass |
private fields, such as plate | The object has them, but the subclass cannot name them; it uses the public accessors |
| public static methods and constants | Yes, and they are reached through either class name |
| constructors | No. A subclass constructor calls one with super |
One row of that table is the one people get wrong in exams: constructors are not inherited. new Car("KP 41 TDE", true) works because Car declares a two-parameter constructor of its own, not because Vehicle has one. Write a subclass with no constructor at all and you get the default no-argument constructor, which immediately tries super() and fails against a superclass like Vehicle.
One array, two kinds of vehicle
The last block of the program is the payoff:
Vehicle[] fleet = {c, t, v};
double takings = 0.0;
for (Vehicle each : fleet) {
System.out.println(" " + each.describe());
takings += each.toll();
}
Two Cars and a Truck in one array, because each of them is-a Vehicle, traversed with the enhanced for loop from Lesson 10 whose variable is declared Vehicle. This is the practical reason inheritance exists: code that works on the general type keeps working when someone adds a new specific type. Add a Motorcycle extends Vehicle tomorrow and the takings loop does not change by one character.
Note what the loop can and cannot do. It can call describe and toll, because Vehicle declares them. It cannot call discountedToll or overweight, for exactly the reason the second compiler error gave. If you need those, either the loop variable has to be the specific type or the superclass has to promise the behaviour, which is where Lesson 16 goes.
How much of this the current exam asks you to write
Be clear-eyed about scope, because it changed. The 2025 course and exam description keeps the ideas: essential knowledge 1.12.A.2 describes putting the common attributes and behaviours of related classes into a superclass and says that subclasses which extend it can draw on those attributes and behaviours, and 1.12.A.3 states that all classes in Java are subclasses of the Object class. Immediately after 1.12.A.2, though, comes an exclusion statement: designing and implementing inheritance relationships are outside the scope of the course and exam.
So the exam can show you a hierarchy and ask what a call prints, which is Lesson 15's subject, and it expects you to know that every object is an Object. It will not ask you to write extends yourself. Practice material written for the earlier framework does ask exactly that, so if you are working through an older book or question bank you will meet subclass-writing questions, and this lesson is what you need for them. Learn it as the Java it is: you cannot read a class hierarchy fluently without having written one.
Common misconceptions
- "A subclass inherits the private fields, so it can use them by name." The object contains them; the subclass code cannot name them. The compiler says
plate has private access in Vehicle, and the accessor is the way in. - "If I do not write super, no superclass constructor runs." One always runs. The compiler inserts
super(), which is why forgetting it produces a message about a constructor you never called. - "super is like this, so I can use it anywhere in the constructor." A
superconstructor call must be the very first statement. Put a println before it and the program will not compile. - "A Vehicle variable holding a Car can do everything a Car can do." Not at compile time. The declared type fixes which methods you may call, whatever the object turns out to be.
- "Car extends Vehicle means Car is bigger than Vehicle, so Vehicle is the special case." The other way round. The subclass is the more specific type and has more, not less: every field and method of the superclass plus its own.
- "Anything can extend anything if the fields fit." Apply the is-a test in words first. A Car is a Vehicle. A Car is not an Engine, so an engine belongs in a field, not in
extends.
What to carry forward
class B extends A says that every B is an A. The B object holds all of A's fields and can call all of A's public methods; A's private fields are in there but only reachable through A's accessors. A B constructor starts by calling an A constructor with super(...), which must be its first statement, and if you leave it out the compiler inserts super() and then complains if A has no matching constructor. Constructors themselves are never inherited. A variable declared A can hold a B, and an A[] can hold a mixture, but through an A variable you may only call what A declares.
The core of it: two types, two questions. What is the object really? What does the variable's declared type allow? Keep those apart and the next lesson is easy; confuse them and nothing in it will make sense.
Next: both classes get a method with the same name, and Java has to choose. The answer is not the one the compiler error above might lead you to expect.
Sources
- Oracle. (n.d.). The Java Tutorials: Inheritance (classes can be derived from other classes, inheriting the fields and methods of those classes; what a subclass may and may not do with private members). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Using the Keyword super (if a constructor does not explicitly invoke a superclass constructor, the compiler inserts a call to the no-argument constructor of the superclass, and a superclass without one produces a compile-time error). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 1, essential knowledge 1.12.A.2 on superclasses and subclasses with its exclusion statement that designing and implementing inheritance relationships are outside the scope of the course and exam, and 1.12.A.3 that all classes in Java are subclasses of Object. apcentral.collegeboard.org
- Wikipedia contributors. (2026). Inheritance (object-oriented programming) (the is-a relationship, and the distinction between inheritance and composition). en.wikipedia.org
- Key terms
- Superclass
- The more general class, named after extends, whose fields and methods a subclass takes on.
- Subclass
- The more specific class, which has everything the superclass has plus whatever it adds.
- extends
- The keyword that declares one class to be a special kind of another.
- super(...)
- A call to a superclass constructor, which must be the first statement of a subclass constructor.
- Is-a relationship
- The test for inheritance: a Car is a Vehicle, so Car may extend Vehicle.
- Declared type
- The type written in a variable's declaration, which decides at compile time which methods may be called.
- Object class
- The class at the top of every Java hierarchy; all classes are subclasses of it.
Overriding and dynamic binding
- Override a superclass method and predict which version runs for a given object.
- Separate the compiler's question, is this call allowed, from the run time's question, whose code runs.
- Use super.method() to reach the version a subclass replaced, and say why plain method() would not do.
- Spot a method that overloads rather than overrides, and read the compiler errors that reject a bad override.
A prediction that feels right and is wrong
A three-axle lorry reaches the toll booth. The describe method is written in Vehicle and nowhere else, and Vehicle's describe ends by calling toll(), whose body in Vehicle is 2.5 * axles. Three axles at 2.50 is 7.50, so describe should report 7.50. Run the program and the lorry pays 11.50.
That prediction is the single most common wrong model of inheritance, and it is worth tracing carefully, because once you see why 11.50 is correct you have the whole of this topic. The reasoning failed at one step: the class a method is written in does not decide which other methods it reaches.
The three classes, and where toll is written
Vehicle is unchanged from Lesson 14 apart from one added method, kind, which exists to make the output readable:
public class Vehicle {
private String plate;
private int axles;
public Vehicle(String plate, int axles) {
this.plate = plate;
this.axles = axles;
}
public String getPlate() {
return plate;
}
public int getAxles() {
return axles;
}
public String kind() {
return "vehicle";
}
public double toll() {
return 2.5 * axles;
}
public String describe() {
return plate + ": " + kind() + " pays " + toll();
}
}
Now the two subclasses, each of which writes its own kind and its own toll:
public class Car extends Vehicle {
private boolean hasTag;
public Car(String plate, boolean hasTag) {
super(plate, 2);
this.hasTag = hasTag;
}
public String kind() {
return "car";
}
public double toll() {
if (hasTag) {
return super.toll() - 1.0;
}
return super.toll();
}
}
public class Truck extends Vehicle {
private int tonnes;
public Truck(String plate, int axles, int tonnes) {
super(plate, axles);
this.tonnes = tonnes;
}
public String kind() {
return "truck";
}
public double toll() {
return super.toll() + 4.0;
}
}
Both subclasses declare a method with the same name, the same parameter list (none) and the same return type as a method in the superclass. That is what overriding means, and Oracle's tutorial states the test exactly: an instance method in a subclass with the same signature, meaning the name plus the number and the type of its parameters, and the same return type as an instance method in the superclass overrides the superclass's method. The 2025 framework says the same thing in its own words when it defines method overriding as a public method in a subclass having the same signature as a public method in the superclass, with behaviour specific to the subclass.
And there is a fourth class, a Bus, which looks as though it overrides toll and does not. Keep it in the corner of your eye:
public class Bus extends Vehicle {
private int seats;
public Bus(String plate, int seats) {
super(plate, 2);
this.seats = seats;
}
public String kind() {
return "bus";
}
public double toll(int passengers) {
return 0.5 * passengers;
}
}
What actually ran
Here is the program that exercises all four, compiled with javac across the five files and run:
public class Main {
public static void main(String[] args) {
Car tagged = new Car("KP 41 TDE", true);
Car plain = new Car("BX 62 LHK", false);
Truck lorry = new Truck("LM 07 RRX", 3, 14);
Bus coach = new Bus("YT 18 SWO", 52);
System.out.println("asked directly:");
System.out.println(" tagged car toll: " + tagged.toll());
System.out.println(" plain car toll: " + plain.toll());
System.out.println(" truck toll: " + lorry.toll());
System.out.println(" bus toll: " + coach.toll());
System.out.println();
System.out.println("describe, which is only written in Vehicle:");
System.out.println(" " + tagged.describe());
System.out.println(" " + lorry.describe());
System.out.println(" " + coach.describe());
System.out.println();
System.out.println("through a Vehicle variable:");
Vehicle v = lorry;
System.out.println(" v.toll() where v holds the truck: " + v.toll());
System.out.println(" v.kind() where v holds the truck: " + v.kind());
System.out.println();
System.out.println("one loop, four vehicles:");
Vehicle[] fleet = {tagged, plain, lorry, coach};
double takings = 0.0;
for (Vehicle each : fleet) {
System.out.println(" " + each.describe());
takings += each.toll();
}
System.out.println("takings: " + takings);
System.out.println();
System.out.println("the bus asked with an argument: " + coach.toll(30));
}
}
Its real output:
asked directly:
tagged car toll: 4.0
plain car toll: 5.0
truck toll: 11.5
bus toll: 5.0
describe, which is only written in Vehicle:
KP 41 TDE: car pays 4.0
LM 07 RRX: truck pays 11.5
YT 18 SWO: bus pays 5.0
through a Vehicle variable:
v.toll() where v holds the truck: 11.5
v.kind() where v holds the truck: truck
one loop, four vehicles:
KP 41 TDE: car pays 4.0
BX 62 LHK: car pays 5.0
LM 07 RRX: truck pays 11.5
YT 18 SWO: bus pays 5.0
takings: 25.5
the bus asked with an argument: 15.0
Take the lorry line by line. lorry.describe() starts running the body of Vehicle.describe, because that is the only describe there is. Inside that body, toll() is really this.toll(), and this is the Truck object. So Java looks for toll starting at the object's own class, finds Truck.toll, and runs it: 2.5 times 3 axles is 7.50, plus the 4.00 surcharge, giving 11.50. The same happens to kind(), which is why the line reads truck rather than vehicle. A method written in the superclass, calling a method the subclass replaced, gets the subclass's version.
This is dynamic binding, also called late binding: the decision about which body to run happens when the call happens, not when the file is compiled. Oracle's page on polymorphism states it in one line: the Java virtual machine calls the appropriate method for the object that is referred to in each variable.
The point: to work out which version of a method runs, find the object, not the code. Ask "what was this actually built as?" and start looking there.
Two types, two jobs
Every call through a variable involves two types, and they answer different questions.
| Question | Answered by | When | Evidence in this lesson |
|---|---|---|---|
| Is this call allowed at all? | the declared type of the variable | compile time | v.discountedToll() in Lesson 14 would not compile |
| Whose body runs? | the class the object was built from | run time | v.toll() gave 11.5, Truck's answer |
Both lines of that table are true of the same statement at the same time. Vehicle v = lorry; v.toll(); compiles because Vehicle declares a toll with no parameters, and it prints 11.5 because the object is a Truck. Neither type is "the real one"; they do different jobs.
The fleet loop is the same fact in useful form. One loop, written once against Vehicle, produced four different calculations because four different objects answered. That is the payoff for the whole module: add a Motorcycle with its own toll tomorrow and the takings loop still does not change.
super.toll, and the mistake of writing toll
Look again at the body of Truck.toll: return super.toll() + 4.0;. The truck's rule is "whatever the standard toll is, plus four", and super.toll() is how you say "the version I replaced". Without it you would have to copy 2.5 * getAxles() into Truck, and then the day the rate changes to 2.75 you would have two places to edit and would find one of them.
Now the trap. Suppose you wrote return toll() + 4.0; instead. Inside a Truck, plain toll() means this.toll(), and dynamic binding sends that straight back to Truck.toll, which calls itself again, and again. There is no base case, so the program does not produce 11.50 or 7.50; it runs until the call stack is exhausted and throws StackOverflowError. Lesson 17 shows that error happening on purpose, with the real stack trace. For now, remember the rule: inside an override, super.name() reaches the replaced version, and plain name() is recursion.
A second point about super: it only reaches one level up, and only from inside the subclass. There is no way to write super.super, and no way for code outside the hierarchy to ask a Truck for Vehicle's toll. Once a subclass overrides a method, outside callers get the override, always.
The bus that overrode nothing
The bus printed a toll of 5.0, which is 2.50 times its two axles: Vehicle's answer, not the 0.50 per passenger its author intended. Look at the signatures side by side.
| In Vehicle | In Bus | Relationship |
|---|---|---|
public double toll() | public double toll(int passengers) | different parameter lists, so different methods |
Because the parameter lists differ, Bus has not replaced anything. It has added a second method that happens to share a name, which is overloading, the thing System.out.println does when it accepts a String or an int or a double. Overloading is settled by the compiler from the arguments at the call site; overriding is settled at run time from the object. So coach.toll() found the inherited no-argument version and returned 5.0, while coach.toll(30) found the new one and returned 15.0. Both printed, both correct, neither what the author wanted.
The lesson is mechanical: to override, copy the header exactly. Same name, same parameter types in the same order, same return type. If you change one letter of the parameter list you have written a new method and the compiler will not warn you, because you have not broken any rule. (Real Java has an annotation, @Override, that asks the compiler to check this for you; it is not part of the exam's Java subset, so no code in this course uses it.)
Two overrides the compiler refuses
Some near-misses do get caught, and the messages are worth recognising. First, dropping public from Car's toll:
double toll() {
return super.toll() - 1.0;
}
Car.java:9: error: toll() in Car cannot override toll() in Vehicle
double toll() {
^
attempting to assign weaker access privileges; was public
1 error
Oracle's rule: the access specifier for an overriding method can allow more, but not less, access than the overridden method. The reason is the fleet loop. Anyone holding a Vehicle may call toll; if a subclass could make its version less accessible, that promise would be broken by whichever object turned up.
Second, changing the return type:
public int toll() {
return 4;
}
Car.java:9: error: toll() in Car cannot override toll() in Vehicle
public int toll() {
^
return type int is not compatible with double
1 error
Same reasoning. Code written against Vehicle stores the result of toll() in a double, and that must keep working whatever object arrives. Note the difference between these two messages and the silent failure of the bus: change the access or the return type and the compiler stops you, because you have tried to override and got it wrong; change the parameter list and it lets you through, because you have written something legal that is simply not an override.
Common misconceptions
- "describe is Vehicle's method, so it calls Vehicle's toll." It calls
this.toll(), andthisis whatever object was built. The lorry proved it: 11.5, not 7.5. - "The declared type decides which version runs." It decides which calls are legal. The object decides which body runs. Both are true at once and neither replaces the other.
- "Overloading and overriding are two words for the same thing." Overloading means same name, different parameter lists, resolved at compile time. Overriding means identical signature in a subclass, resolved at run time. The bus is what happens when you mean one and write the other.
- "Inside an override I can call the old version by its name." Plain
toll()is this object'stoll, which is the method you are standing in. Usesuper.toll(). - "A subclass can tighten access to protect its version." The compiler refuses: an override may widen access, never narrow it.
- "Casting the variable back to Truck is how I get the right toll." No cast is needed, and casting object types is outside this exam's subset anyway. Dynamic binding already gives you the Truck's version through a Vehicle variable.
Where this leaves us
An override is a method in a subclass with the same name, parameter list and return type as one in its superclass. When a call is made, the compiler checks it against the variable's declared type and the virtual machine runs the version belonging to the object that actually arrived, even if the call sits inside a superclass method. super.name() reaches the replaced version from inside the override and is how a subclass says "the usual rule, plus my difference". A different parameter list is not an override but an overload, and nothing will warn you; a narrower access modifier or a changed return type is an override the compiler rejects outright.
In short: the compiler asks what the variable promises, and the run time asks what the object is. Write those two questions in the margin of every polymorphism question and you will get it right.
One thing is still missing. Vehicle had to invent a toll of its own so that subclasses would have something to override, and kind returns the useless word vehicle for an object that is never merely a vehicle. The next lesson deals with superclasses that declare a method without writing a body at all.
Sources
- Oracle. (n.d.). The Java Tutorials: Overriding and Hiding Methods (an instance method with the same signature and return type as one in the superclass overrides it; the access specifier of an overriding method can allow more, but not less, access). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Polymorphism (the Java virtual machine calls the appropriate method for the object that is referred to in each variable). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 1, essential knowledge 1.15.A.5, which defines method overriding as a public method in a subclass having the same signature as a public method in the superclass with behaviour specific to the subclass. apcentral.collegeboard.org
- Wikipedia contributors. (2026). Dynamic dispatch (choosing which implementation of a polymorphic operation to call at run time rather than at compile time). en.wikipedia.org
- Key terms
- Overriding
- Writing a method in a subclass with the same name, parameter list and return type as one in the superclass, so it replaces it.
- Method signature
- A method's name together with the number and types of its parameters, which is what decides whether two methods are the same.
- Overloading
- Two methods with the same name and different parameter lists, chosen by the compiler from the arguments.
- Dynamic binding
- Choosing which version of an overridden method runs from the object, at the moment of the call.
- super.method()
- A call from inside a subclass to the superclass version of a method the subclass has overridden.
- Polymorphism
- One call, written once against a general type, running different code for different objects.
- Run-time type
- The class an object was actually built from, as opposed to the type written in the variable's declaration.
Abstract classes and interfaces as the exam uses them
- Declare an abstract class with an abstract method, and say why it cannot be instantiated.
- Use an abstract method to make the compiler insist that every subclass supplies a behaviour.
- Write an interface and implement it in two classes that share no superclass.
- Compare a concrete class, an abstract class and an interface, and say which the current exam expects you to write.
The word vehicle, which no vehicle ever was
In Lesson 15 the coach paid 5.00 at the toll booth because Bus declared toll(int passengers) and nobody noticed that this overrode nothing. Look at why the mistake was possible. Vehicle.toll existed only so that subclasses would have something to replace, and Vehicle.kind returned the string vehicle, a word that described none of the four objects in the program. The superclass was making up answers it had no business having, and one of them quietly became the bus's fare.
Java has a way of saying "every vehicle has a toll, and I refuse to guess what it is". That is what abstract means, and by the end of this lesson the bus bug will be a compiler error rather than a wrong number.
An abstract method has no body; an abstract class cannot be built
Here is a shape hierarchy. Read the two halves of Shape separately: getLabel and summary are ordinary methods with ordinary bodies, and area is a declaration with a semicolon where the braces should be.
public abstract class Shape implements Measurable {
private String label;
public Shape(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
public abstract double area();
public String summary() {
return getLabel() + " has area " + area();
}
}
Oracle's tutorial defines it in the same terms: an abstract method is one declared without an implementation, without braces and followed by a semicolon, and if a class includes an abstract method then the class itself must be declared abstract. Abstract classes cannot be instantiated, but they can be subclassed.
summary is the interesting line. It calls area(), a method with no body anywhere in this class, and it compiles. It is allowed to, because dynamic binding from the last lesson guarantees that by the time summary actually runs, this will be some real object whose class did supply an area. The superclass is free to use a behaviour it has only promised.
Two subclasses keep that promise:
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String label, double width, double height) {
super(label);
this.width = width;
this.height = height;
}
public double area() {
return width * height;
}
}
public class RightTriangle extends Shape {
private double base;
private double height;
public RightTriangle(String label, double base, double height) {
super(label);
this.base = base;
this.height = height;
}
public double area() {
return base * height / 2.0;
}
}
Try to build the abstract class itself and the compiler stops you:
Shape s = new Shape("mystery");
Main.java:3: error: Shape is abstract; cannot be instantiated
Shape s = new Shape("mystery");
^
1 error
That message is a feature. There is no such thing as a shape in general with an area in general, and now there is no such object either. What you can still do is declare a variable of type Shape, or an array of them, and fill it with real subclass objects. Shape works perfectly well as a type; it just cannot be a thing.
The error that makes the bus impossible
Give Rectangle a method called areaOf instead of area, the sort of slip that produced the bus, and the program no longer compiles:
Rectangle.java:1: error: Rectangle is not abstract and does not override abstract method area() in Shape
public class Rectangle extends Shape {
^
1 error
Compare that with Lesson 15, where Bus compiled cleanly and produced a wrong fare that only a careful reader of the output would catch. One keyword moved the failure from run time to compile time, and from a number that looks plausible to a message that names the class, the method and the reason. This is the strongest practical argument for abstract methods, and it is worth stating as a rule: if a superclass has no sensible default, do not invent one.
Why this matters: a compiler error costs you a minute. A plausible wrong number in a program you have already shipped costs whatever the bus fares add up to.
An interface: the promise without any of the machinery
An abstract class can still hold fields and a constructor, as Shape holds label. Sometimes you want only the promise, and you want it from classes that have nothing else in common. That is an interface. Oracle's overview describes it as a group of related methods with empty bodies, forming a contract between a class and the outside world which the compiler enforces at build time.
public interface Measurable {
double area();
String getLabel();
}
No fields, no constructor, no bodies, and no public needed on the methods: everything an interface declares is public and abstract already. A class promises to keep the contract with implements:
public class Plot implements Measurable {
private String name;
private double hectares;
public Plot(String name, double hectares) {
this.name = name;
this.hectares = hectares;
}
public String getLabel() {
return name + " (plot)";
}
public double area() {
return hectares * 10000.0;
}
}
A Plot is a piece of land, measured in hectares, converted to square metres. It is not a Shape, it extends nothing, and it shares no field or method body with Rectangle. But both can be measured, so both can go through one method:
public static double totalArea(Measurable[] items) {
double total = 0.0;
for (Measurable m : items) {
total += m.area();
}
return total;
}
public static Measurable largest(Measurable[] items) {
Measurable best = items[0];
for (Measurable m : items) {
if (m.area() > best.area()) {
best = m;
}
}
return best;
}
The main program builds a rectangle of 4.0 by 3.5 metres, a right triangle with a base of 6.0 and a height of 2.0, and a plot of a quarter of a hectare, then walks them twice: once as Shapes, once as Measurables. Its real output:
through the abstract superclass:
kitchen floor has area 14.0
gable end has area 6.0
total shape area: 20.0
through the interface, including something that is not a Shape:
total measured area: 2520.0
kitchen floor: 14.0
gable end: 6.0
Ten Acre (plot): 2500.0
largest of the three: Ten Acre (plot)
Check the arithmetic: 4.0 times 3.5 is 14.0, 6.0 times 2.0 halved is 6.0, and a quarter of a hectare is 2500 square metres, so 2520.0 is right. The point is not the sum but what the Measurable[] could hold that a Shape[] could not. Inheritance gives you one line of descent; an interface cuts across the lines. And largest shows the other half of the bargain: a method may return a Measurable without knowing or caring which class it hands back.
Break the contract and you get the same kind of message as before. Delete getLabel from Plot:
Plot.java:1: error: Plot is not abstract and does not override abstract method getLabel() in Measurable
public class Plot implements Measurable {
^
1 error
The three of them side by side
| Ordinary class | Abstract class | Interface | |
|---|---|---|---|
Can you write new on it? | Yes | No | No |
| Method bodies? | All of them | Some may be missing | None, in this course's subset |
| Fields and a constructor? | Yes | Yes | No |
| How many can one class take on? | It extends one class | It extends one class | It can implement several |
| What does the subclass owe? | Nothing | A body for every abstract method | A body for every method declared |
| Can it be a variable's type? | Yes | Yes | Yes |
The last two rows are the ones that decide which you reach for. If the classes share code as well as a promise, an abstract class saves you writing it twice: Shape stores the label and writes summary once. If the classes share nothing but a promise, or a class needs to keep promises to two different pieces of code, an interface is the only option, because Java lets a class extend exactly one class and implement as many interfaces as it likes.
Where you have been using this already
None of this is exotic; the library you have been calling since Lesson 3 is built out of it. String, according to its API page, implements Comparable of String, and the compareTo method on your exam reference sheet is the one that interface demands. ArrayList, on its own page, is described as the resizable-array implementation of the List interface, which is why professional Java so often declares a variable as a List and creates an ArrayList for it. You have been programming against interfaces without being told.
What the current exam asks of you
Now the scope, plainly. Search the 2025 course and exam description for a topic on abstract classes or interfaces and you will not find one: its four units run from using objects and methods to data collections, and the closest it comes to inheritance is essential knowledge 1.12.A.2, whose exclusion statement puts designing and implementing inheritance relationships outside the scope of the course and exam. So no free-response question is going to ask you to write public abstract class, and no multiple-choice question will hinge on the number of interfaces a class may implement.
Why learn it, then? Three reasons, and the first is the honest one. This is the vocabulary of every Java course, textbook and job that comes after this one, and you are three pages from having it. The second is that the material you practise on may be older than the 2025 framework, and older question banks do use these features. The third is the one that shows up in your own code tomorrow: an abstract method is how you stop a superclass from inventing an answer, which is a bug you have now watched happen.
Common misconceptions
- "An abstract class is useless because you cannot make one." You cannot instantiate it; you can declare variables and arrays of it, put subclass objects in them, and write methods that work on any of them. That is most of what it is for.
- "An abstract method is an empty method." An empty method has braces with nothing between them and quietly returns nothing. An abstract method has a semicolon and no body at all, and forces every concrete subclass to supply one.
- "A class can extend several classes if you separate them with commas." It cannot. One superclass, as many interfaces as you like: that asymmetry is a deliberate design decision in Java and the reason interfaces exist.
- "An interface is just an abstract class with nothing in it." They differ where it counts: an abstract class can carry fields, a constructor and finished methods, and a class may only have one of them. An interface carries none of that and a class may implement several.
- "implements and extends are interchangeable words." A class extends a class and implements an interface. Swap them and the compiler rejects the file.
- "Since it is not on the exam, it does not matter." The compiler error that caught
areaOfis the same error that would have caught the bus. Knowing when to make a superclass refuse to guess is worth more than one exam's worth of marks.
What to remember
abstract on a method means a declaration with a semicolon and no body, and it forces every concrete subclass to supply one, on pain of a compiler error that names the class and the method. abstract on a class means the class cannot be instantiated, though it may still hold fields, a constructor and finished methods, and may still be used as the type of a variable or an array. An interface is the promise with none of the machinery: no fields, no constructor, no bodies, and a class may implement as many as it needs while extending only one class. Both let you write one method against the general type and have it work on objects you had not thought of yet, and both turn a missing behaviour into a compile-time failure instead of a plausible wrong number.
Bottom line: use an abstract class when the subclasses share code as well as a promise, and an interface when they share only the promise. Neither appears in the 2025 framework's topic list, and both are the next thing you will need.
That closes Module 6. Module 7 turns to the algorithms the exam does test hardest: a method that calls itself, the search that halves the problem each time, and three sorts whose comparisons you will count.
Sources
- Oracle. (n.d.). The Java Tutorials: Abstract Methods and Classes (an abstract method is declared without an implementation, a class with one must itself be abstract, and abstract classes cannot be instantiated but can be subclassed). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: What Is an Interface? (an interface is a group of related methods with empty bodies, forming a contract enforced at build time by the compiler). docs.oracle.com
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class ArrayList (described as the resizable-array implementation of the List interface). docs.oracle.com
- College Board. (2025). AP Computer Science A Course and Exam Description, Course Framework units 1 to 4 (no topic on abstract classes or interfaces appears) and essential knowledge 1.12.A.2 with its exclusion statement on designing and implementing inheritance relationships. apcentral.collegeboard.org
- Wikipedia contributors. (2026). Abstract type (a type that cannot be instantiated directly and whose operations may be declared without implementations). en.wikipedia.org
- Key terms
- Abstract method
- A method declared with a semicolon and no body, which every concrete subclass must supply.
- Abstract class
- A class that cannot be instantiated, usually because it declares at least one abstract method.
- Interface
- A named set of method declarations with no bodies, fields or constructor, which a class promises to supply.
- implements
- The keyword by which a class takes on an interface's contract; a class may implement several.
- Contract
- The set of methods a type promises callers, enforced by the compiler rather than by good intentions.
- Concrete class
- An ordinary class with a body for every method, so objects of it can be created.
- Comparable
- A library interface whose compareTo method String implements, which is why compareTo is on the exam reference sheet.
Module 7: Recursion, Searching, and Sorting
Four lessons on the algorithms the exam asks you to trace. A method that calls itself, watched entering and leaving through printed output and then driven off the end of the call stack on purpose. Binary search, written both ways, with its comparisons counted against linear search on the same data. Selection sort and insertion sort side by side on the same five numbers, with their comparison counts printed. Then merge sort, the recursive sort, traced through every split and merge.
Recursion: the call stack traced by hand and printed
- Identify the base case and the recursive call in a method, and say what makes the recursion end.
- Trace a recursive call by hand and confirm the trace against printed output.
- Explain why each call has its own parameters, and what happens when the base case is never reached.
- Write recursive methods over an int, a String and an array, and compare a recursive solution with a loop.
Twenty-four, built on the way back out
Four factorial is 4 times 3 times 2 times 1, which is 24. Here is a method that computes it with no loop at all, printing a line as it enters each call and another as it leaves, indented by how deep it has gone:
public static int factorial(int n, int depth) {
String pad = "";
for (int i = 0; i < depth; i++) {
pad += " ";
}
System.out.println(pad + "enter factorial(" + n + ")");
if (n == 0) {
System.out.println(pad + "base case, return 1");
return 1;
}
int smaller = factorial(n - 1, depth + 1);
int result = n * smaller;
System.out.println(pad + "return " + n + " * " + smaller + " = " + result);
return result;
}
Its real output for factorial(4, 0):
factorial(4) step by step:
enter factorial(4)
enter factorial(3)
enter factorial(2)
enter factorial(1)
enter factorial(0)
base case, return 1
return 1 * 1 = 1
return 2 * 1 = 2
return 3 * 2 = 6
return 4 * 6 = 24
answer: 24
Read it as a V. The left side going down is five calls being started, each one asking a smaller question and then waiting. The bottom is the one call that does not ask anything: factorial(0) returns 1 immediately. The right side coming back up is the multiplications, and they happen in the reverse order of the questions: 1 times 1, then 2 times 1, then 3 times 2, then 4 times 6. Nothing was multiplied on the way down. Every multiplication waited for an answer from below.
That is the single most useful picture in this topic. When you trace recursion by hand, write the calls down the page as you meet them and then work back up the page filling in the returns.
The two parts, and what each one is for
The 2025 framework's definition is worth having word for word: a recursive method is a method that calls itself, recursive methods contain at least one base case, which halts the recursion, and at least one recursive call, and recursion is another form of repetition.
| Part | In factorial | Its job |
|---|---|---|
| Base case | if (n == 0) return 1; | answers without calling again, so the process stops |
| Recursive call | factorial(n - 1, depth + 1) | asks a smaller version of the same question |
| Combination | n * smaller | turns the smaller answer into this one |
The recursive call must move towards the base case. n - 1 does, because starting from any non-negative n you eventually reach 0. Write factorial(n) instead of factorial(n - 1) and the question never gets smaller.
The framework's next point explains why the trace above works: each recursive call has its own set of local variables, including the parameters, and those parameter values capture the progress of the recursion much as a loop control variable captures the progress of a loop. There are five different n values alive at the deepest point of that trace, one per call, each in its own frame on the call stack. That is also why the printed pad works: each call has its own.
The point: a recursive call is an ordinary method call. Nothing new happens. The only unusual thing is that the method it calls happens to be the one it is already inside.
Where the int runs out
Strip the printing out and run the plain version from 1 to 13:
public static int plain(int n) {
if (n == 0) {
return 1;
}
return n * plain(n - 1);
}
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 1932053504
Twelve factorial is 479,001,600 and thirteen factorial is 6,227,020,800, which is not what the program printed. The int overflow from Lesson 2 has arrived: 6,227,020,800 is past Integer.MAX_VALUE, so the multiplication wrapped round and produced 1,932,053,504. The recursion is perfectly correct and the arithmetic is not. It is a good reminder that a correct algorithm in a type that is too small is still a wrong program.
What happens when the base case is never reached
Here is a countdown whose base case is missing, with a print every thousandth call so you can see how far it gets:
public static int countdown(int n) {
if (n % 1000 == 0) {
System.out.println(" reached n = " + n);
}
return countdown(n - 1);
}
Called with 3, its real output ended like this:
reached n = -8000
reached n = -9000
reached n = -10000
Exception in thread "main" java.lang.StackOverflowError
at Deep.countdown(Deep.java:8)
at Deep.countdown(Deep.java:11)
at Deep.countdown(Deep.java:11)
at Deep.countdown(Deep.java:11)
at Deep.countdown(Deep.java:11)
On the run shown, it sailed straight past 0 into the negatives and got beyond ten thousand calls deep before the stack ran out, and the printed trace listed 1,019 frames of countdown before the virtual machine stopped printing. Every one of those frames is a call that started and never finished, each holding its own n. That is what the call stack is: the record of calls in progress, and it is not unlimited.
StackOverflowError, whose API page describes it as thrown when a stack overflow occurs because an application recurses too deeply, is the signature of a broken base case. If you see it, do not look for a subtle logic error. Look at two things: does the base case exist, and does every recursive call move towards it? Here the answer to the first question was no.
Recursion on a String
Recursion suits anything you can make one step smaller. A String can be shortened with substring, which you met in Lesson 3, so both of these work by handing on a shorter String:
public static String reverse(String s) {
if (s.length() <= 1) {
return s;
}
return reverse(s.substring(1)) + s.substring(0, 1);
}
public static boolean isPalindrome(String s) {
if (s.length() <= 1) {
return true;
}
if (!s.substring(0, 1).equals(s.substring(s.length() - 1))) {
return false;
}
return isPalindrome(s.substring(1, s.length() - 1));
}
reverse("stressed") = desserts
reverse("") = []
reverse("a") = a
palindrome checks:
level -> true
levels -> false
abba -> true
x -> true
empty -> true
Two things to take from that. First, reverse peels the first character off, reverses what is left, and puts the character on the end: the work again happens on the way back out. Second, look at the base cases. Both methods stop at length 1 or less, which covers the empty String as well, and that is why reverse("") printed nothing between the brackets instead of throwing. A base case of s.length() == 1 would have sent the empty String on to substring(1) and broken it.
isPalindrome has two stopping conditions of different kinds: a base case that says yes, and an early return that says no. Both are needed, and only the first counts as the base case, because only it halts the recursion at the bottom of a chain of calls.
Recursion on an array, with a helper parameter
An array cannot be made shorter, so instead of shrinking the data you move an index. That extra parameter is the standard trick:
public static int sum(int[] values, int from) {
sumCalls++;
if (from == values.length) {
return 0;
}
return values[from] + sum(values, from + 1);
}
public static int max(int[] values, int from) {
if (from == values.length - 1) {
return values[from];
}
int rest = max(values, from + 1);
if (values[from] > rest) {
return values[from];
}
return rest;
}
On the array 14, 8, 22, 9, 7 the real output was:
recursive sum of 14 8 22 9 7: 60
recursive max of 14 8 22 9 7: 22
calls made by sum: 6
Six calls for five elements, because there is one call per element plus the one that finds from == values.length and returns 0. Compare the base cases: sum stops one past the end and returns 0, which is the right answer for no elements, while max stops on the last element, because the largest of no elements is not a number at all.
The framework also notes that any recursive solution can be replicated iteratively and the other way round. The loop version of sum is three lines and does not consume stack. Recursion earns its place when the problem is naturally described in terms of itself, which is why it is the natural shape for binary search and merge sort in the next two lessons, and an odd choice for adding up an array.
What the exam does with recursion
Topic 4.16 asks you to determine the result of calling recursive methods, and carries an exclusion statement: writing recursive code is outside the scope of the course and exam. So the questions you will be set are tracing questions. You will be handed a method and asked what mystery(5) returns, or how many times a method is called, or what gets printed and in what order.
That makes the V-shaped trace above the actual exam technique, not a teaching aid. Two habits pay for themselves in the exam room. Write the calls in a column, one per line, indenting as you go, and only start filling in return values when you reach the base case. And check whether the work happens before or after the recursive call: print then recurse gives one order and recurse then print gives the reverse, and telling those apart is most of what the printing questions are about.
Common misconceptions
- "A recursive method repeats itself, so it is a loop with different punctuation." It is repetition, but each repetition is a new call with its own parameters and its own place to come back to. A loop reuses one set of variables; five nested calls have five.
- "The base case is the last thing that happens." It is the last call to start and the first to finish. Everything after it in time is the return journey.
- "StackOverflowError means the program ran out of memory." It means the stack of unfinished calls got too deep. The cause is almost always a base case that is missing or never reached, not a large array.
- "factorial(n) calling factorial(n) is fine because n changes inside." The argument is evaluated before the call, so if you pass
nrather thann - 1the new call gets exactly what this one got, forever. - "Recursion is always slower and should be avoided." It has a real cost per call, and the Fibonacci activity shows that cost getting out of hand. But merge sort is recursive and beats any of the loop-based sorts in this course on large arrays, as Lesson 20 measures.
Pulling it together
A recursive method calls itself on a smaller version of the same problem and has at least one base case that answers without calling again. Each call gets its own parameters and local variables, and the calls stack up until the base case is reached, after which the answers are combined on the way back out: the trace looks like a V and the combining happens on the right-hand side. A missing or unreachable base case gives StackOverflowError, whose stack trace is a wall of the same line. A String recurses by getting shorter with substring; an array recurses by moving an index parameter. Every recursion can be written as a loop and every loop as a recursion, so the question is always which one describes the problem more honestly.
Worth holding on to: two questions answer nearly every recursion item on the exam. Where does it stop, and does the work happen before or after the call?
Lessons 18 and 20 are the two places where recursion is not a stylistic choice. Binary search halves a sorted array until the answer appears, and merge sort splits an array until sorting is trivial and then puts the pieces back in order.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topic 4.16 Recursion, essential knowledge 4.16.A.1 to 4.16.A.3 (a recursive method calls itself and contains at least one base case and one recursive call; each call has its own local variables including the parameters; any recursive solution can be replicated iteratively) with the exclusion statement that writing recursive code is outside the scope of the course and exam. apcentral.collegeboard.org
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class StackOverflowError (thrown when a stack overflow occurs because an application recurses too deeply). docs.oracle.com
- Wikipedia contributors. (2026). Recursion (computer science) (base case and recursive case, and the way a recursive definition mirrors the structure of the data). en.wikipedia.org
- Wikipedia contributors. (2026). Call stack (the record of active calls, each with its own frame holding parameters and local variables). en.wikipedia.org
- Key terms
- Recursive method
- A method that calls itself on a smaller version of the same problem.
- Base case
- The case that answers without calling again, which is what halts the recursion.
- Recursive call
- The call a method makes to itself, which must move the problem towards the base case.
- Call stack
- The record of calls that have started and not yet finished, one frame each.
- Frame
- One call's own storage for its parameters and local variables, separate from every other call's.
- StackOverflowError
- The error thrown when calls stack too deeply, nearly always a base case that is never reached.
- Helper parameter
- An extra parameter, usually an index, that lets a recursion walk data it cannot make smaller.
Binary search, recursive and iterative
- Trace binary search on a sorted array by recording low, high and mid at every look, for a target that is present and for one that is not.
- Write binary search both recursively and with a while loop, and explain why the two versions examine the same elements in the same order.
- Explain why the data must be sorted, and predict what goes wrong when it is not or when the loop condition loses its equals sign.
- Compare how many elements linear and binary search examine as an array grows, and connect the difference to repeated halving.
A million numbers and twenty looks
An array holds the 1,048,575 even numbers from 0 to 2,097,148, in increasing order. A program searched it for the last of them in two ways, once by checking every element from the front and once by a method called binary search. A counter went up by one every time either search read an element and compared it with the target. Call each of those a look. This is what it printed:
1048575 even numbers, from 0 to 2097148
linear search for 2097148: index 1048574 after 1048575 looks
binary search for 2097148: index 1048574 after 20 looks
binary search for 999999: index -1 after 20 looks
The linear search is the one from Lesson 10, and its count is no surprise: the target sat in the last place, so it read every element. Binary search needed twenty. The last line is stranger. 999,999 is odd, so it is nowhere in the array, and binary search was certain of that after twenty looks, without reading more than a million of the elements. How can a method be sure a number is absent from places it never looked?
Because the array is sorted. In a sorted array, one look at the middle element tells you something about every other element. The middle of this array holds 1,048,574. If you want 999,999, everything to the right of the middle is bigger still, so half the array is ruled out by one comparison. Do the same to the half that is left, and again, and a million elements run out after twenty halvings. That is the whole idea of binary search. The rest of this lesson makes it exact, because the exam asks exact questions: which elements are examined, in what order, and how many times the loop runs.
Fifteen numbers, searched by hand
Start with an array small enough to search on paper. Here are fifteen sorted numbers and their indexes:
| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| value | 3 | 8 | 12 | 17 | 23 | 29 | 31 | 38 | 42 | 47 | 55 | 61 | 68 | 74 | 80 |
Look for 61. Two variables mark the part of the array where 61 could still be: low starts at 0 and high at 14, which is the whole array. The middle position is mid = (low + high) / 2, which is 7, and index 7 holds 38.
38 is less than 61. The array is sorted, so every element at index 7 or before is 38 or less, and none of them can be 61. Throw all eight away by moving low to mid + 1, which is 8. The range is now 8 to 14, its middle is (8 + 14) / 2 = 11, and index 11 holds 61. Found, after two looks. Linear search, starting from index 0, would have needed twelve.
Here is a method that does exactly that, written recursively, with a print line so you can watch it work. It comes from a program called BinaryTrace.java:
public static int binary(int[] a, int target, int low, int high) {
if (low > high) {
System.out.println(" low " + low + " is past high " + high + ": nothing left");
return -1;
}
int mid = (low + high) / 2;
looks++;
System.out.println(" low " + low + ", high " + high + ", mid " + mid + ", a[mid] " + a[mid]);
if (a[mid] == target) {
return mid;
}
if (a[mid] > target) {
return binary(a, target, low, mid - 1);
}
return binary(a, target, mid + 1, high);
}
Its real output for 61, and then for 60, which is not in the array:
recursive binary search for 61:
low 0, high 14, mid 7, a[mid] 38
low 8, high 14, mid 11, a[mid] 61
returned 11 after 2 looks
recursive binary search for 60:
low 0, high 14, mid 7, a[mid] 38
low 8, high 14, mid 11, a[mid] 61
low 8, high 10, mid 9, a[mid] 47
low 10, high 10, mid 10, a[mid] 55
low 11 is past high 10: nothing left
returned -1 after 4 looks
Follow the search for 60 one line at a time. 38 is too small, so low becomes 8. 61 is too big, so high becomes mid - 1, which is 10. 47 is too small, so low becomes 10. The range is now the single index 10, which holds 55, too small again, so low becomes 11. Now low is 11 and high is 10. A range that runs from 11 up to 10 contains no index at all: every place 60 could have been has been ruled out, and the method returns -1.
So this method has two ways to stop, where each recursion in Lesson 17 had one. Found: the middle element equals the target, so return its index. Empty: low has passed high, so return -1. Every other call hands on a smaller range, because mid - 1 and mid + 1 both leave out the element just examined, and a range that shrinks by at least one every call must eventually either hit the target or run out. The course framework puts the algorithm in one sentence: binary search starts at the middle of a sorted array or ArrayList and eliminates half of it in each recursive call until the desired value is found or all elements have been eliminated.
Key idea: low and high are not places you are standing. They are the two ends of the region where the target could still be, and every look moves one of them past mid.
The same search as a while loop
The framework says binary search can be written either iteratively or recursively, and exam questions use both. In the loop version the two parameters become two local variables, and each recursive call becomes an assignment:
public static int loopBinary(int[] a, int target) {
int low = 0;
int high = a.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
looks++;
System.out.println(" low " + low + ", high " + high + ", mid " + mid + ", a[mid] " + a[mid]);
if (a[mid] == target) {
return mid;
}
if (a[mid] > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
System.out.println(" low " + low + " is past high " + high + ": nothing left");
return -1;
}
iterative binary search for 60:
low 0, high 14, mid 7, a[mid] 38
low 8, high 14, mid 11, a[mid] 61
low 8, high 10, mid 9, a[mid] 47
low 10, high 10, mid 10, a[mid] 55
low 11 is past high 10: nothing left
returned -1 after 4 looks
The same five lines, look for look. That is the translation working, and it is worth seeing exactly where each piece went:
| Recursive version | Loop version |
|---|---|
parameters low and high | local variables low and high |
first call passes 0 and a.length - 1 | variables start at 0 and a.length - 1 |
base case: if low > high, return -1 | loop runs while low <= high, then returns -1 |
return binary(a, target, low, mid - 1); | high = mid - 1; |
return binary(a, target, mid + 1, high); | low = mid + 1; |
The third row is the one people garble. The recursion stops when low > high; the loop keeps going while low <= high. Those are exact opposites, as they must be, because the loop condition is the condition for not yet being in the base case. This is Lesson 17's claim that any recursion can be written as a loop, made concrete. Here the loop is arguably the plainer of the two, because nothing is left waiting when a recursive call returns: each call simply passes back whatever the next call gives it.
The rest of BinaryTrace.java is a linear search that counts its looks and a main method that runs everything:
public class BinaryTrace {
private static int looks = 0;
public static void main(String[] args) {
int[] data = {3, 8, 12, 17, 23, 29, 31, 38, 42, 47, 55, 61, 68, 74, 80};
looks = 0;
System.out.println("linear search for 61 returned " + linear(data, 61)
+ " after " + looks + " looks");
looks = 0;
System.out.println("linear search for 60 returned " + linear(data, 60)
+ " after " + looks + " looks");
System.out.println();
System.out.println("recursive binary search for 61:");
looks = 0;
int where = binary(data, 61, 0, data.length - 1);
System.out.println("returned " + where + " after " + looks + " looks");
System.out.println();
System.out.println("recursive binary search for 60:");
looks = 0;
where = binary(data, 60, 0, data.length - 1);
System.out.println("returned " + where + " after " + looks + " looks");
System.out.println();
System.out.println("iterative binary search for 60:");
looks = 0;
where = loopBinary(data, 60);
System.out.println("returned " + where + " after " + looks + " looks");
}
public static int linear(int[] a, int target) {
for (int i = 0; i < a.length; i++) {
looks++;
if (a[i] == target) {
return i;
}
}
return -1;
}
// binary and loopBinary, exactly as printed above
}
The first two lines of its output were the linear searches:
linear search for 61 returned 11 after 12 looks
linear search for 60 returned -1 after 15 looks
Fifteen looks to be sure 60 is absent, against binary search's four. Linear search cannot stop early, because in data it does not assume is sorted, the one element it has not read might be the one.
Why twenty looks is enough
Each look removes the middle element and one of the two halves beside it. With 15 elements, the halves either side of the middle hold 7 each; with 7, they hold 3; then 1; then none. That is four looks at most, which is why the search for 60 took four. To see the pattern across sizes, the next program uses the loop version with the printing taken out, which the next three programs all share:
public static int binarySearch(int[] a, int target) {
int low = 0;
int high = a.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
looks++;
if (a[mid] == target) {
return mid;
}
if (a[mid] > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
Sizes.java builds sorted arrays of even numbers of many sizes, searches each one for every number from -1 up to twice its size, present and absent alike, and keeps the largest count of looks:
public class Sizes {
private static int looks = 0;
public static void main(String[] args) {
int[] sizes = {1, 2, 3, 4, 7, 8, 15, 16, 100, 1000, 1000000};
for (int n : sizes) {
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 2 * i;
}
int worst = 0;
for (int t = -1; t <= 2 * n; t++) {
looks = 0;
binarySearch(a, t);
if (looks > worst) {
worst = looks;
}
}
System.out.println("n = " + n + ": binary search at most " + worst
+ ", linear search at most " + n);
}
}
// binarySearch, exactly as printed above
}
n = 1: binary search at most 1, linear search at most 1
n = 2: binary search at most 2, linear search at most 2
n = 3: binary search at most 2, linear search at most 3
n = 4: binary search at most 3, linear search at most 4
n = 7: binary search at most 3, linear search at most 7
n = 8: binary search at most 4, linear search at most 8
n = 15: binary search at most 4, linear search at most 15
n = 16: binary search at most 5, linear search at most 16
n = 100: binary search at most 7, linear search at most 100
n = 1000: binary search at most 10, linear search at most 1000
n = 1000000: binary search at most 20, linear search at most 1000000
Read the binary figures. 1 element needs 1 look, 3 need 2, 7 need 3, 15 need 4: every time the array doubles and gains one more, the worst case goes up by one. Sizes 8 and 16, one past those, need one look more than 7 and 15. In general the worst case is the number of times n can be halved before nothing is left, written floor(log2 n) + 1. For a million that is 20; a billion elements, a thousand times more, would need only 30. Linear search's worst case is n itself, so it grows exactly as fast as the data does.
Now the opening numbers explain themselves. 1,048,575 is 220 - 1, the largest size that twenty halvings can clear, which is why both of its binary searches took exactly twenty. Here is that program:
public class Big {
private static int looks = 0;
public static void main(String[] args) {
int size = 1048575;
int[] big = new int[size];
for (int i = 0; i < size; i++) {
big[i] = 2 * i;
}
int last = big[size - 1];
System.out.println(size + " even numbers, from 0 to " + last);
looks = 0;
int where = linear(big, last);
System.out.println("linear search for " + last + ": index " + where + " after " + looks + " looks");
looks = 0;
where = binary(big, last, 0, size - 1);
System.out.println("binary search for " + last + ": index " + where + " after " + looks + " looks");
looks = 0;
where = binary(big, 999999, 0, size - 1);
System.out.println("binary search for 999999: index " + where + " after " + looks + " looks");
}
// linear as in BinaryTrace; binary as above, without its two print lines
}
The framework's wording is careful: binary search is typically more efficient than linear search. Typically, not always. In the fifteen-number array, linear search finds 3 on its first look, while binary search needs four, visiting 38, 17 and 8 before it reaches 3. What binary search guarantees is a small worst case, not a win on every target.
What unsorted data does to it
Everything so far rests on one assumption: when the middle element is too small, everything to its left is too small as well. Take the sorting away and the assumption is false, but the code has no way of knowing. Unsorted.java uses binarySearch and the counting linear from before, with this main:
public static void main(String[] args) {
int[] jumbled = {42, 8, 74, 3, 61, 29, 17};
System.out.println("binary search for 61 returned " + binarySearch(jumbled, 61));
System.out.println("linear search for 61 returned " + linear(jumbled, 61));
int found = 0;
for (int v : jumbled) {
if (binarySearch(jumbled, v) != -1) {
found++;
}
}
System.out.println("binary search found " + found + " of the " + jumbled.length + " values");
}
binary search for 61 returned -1
linear search for 61 returned 4
binary search found 2 of the 7 values
Trace the search for 61. low 0, high 6, mid 3, which holds 3. 3 is less than 61, so indexes 0 to 3 are thrown away and low moves to 4. Now mid is 5, which holds 29, also less than 61, so low moves to 6, and in that step index 4, the one holding 61, goes out with the rest of the left half. The last look, at index 6, finds 17, and the range is empty. The code broke none of its own rules. Its rules were written for sorted data, and given anything else it fails quietly, returning -1 for a value that is sitting right there. Of the seven values, it found only 3 and 29, the two that happen to be where the halving lands.
Java's own library says the same about its built-in binary search: the array must be sorted before the call, and if it is not, the results are undefined. On the exam this appears as a precondition, a condition that must be true before code can be relied on. The framework states it directly: data must be in sorted order to use the binary search algorithm.
The loop condition that loses eight values out of fifteen
The commonest way to break a correct binary search is to drop the equals sign from the loop condition. It looks harmless, because "while low is less than high" sounds like "while there is something left".
public class OffByOne {
public static void main(String[] args) {
int[] data = {3, 8, 12, 17, 23, 29, 31, 38, 42, 47, 55, 61, 68, 74, 80};
System.out.print("values the broken search cannot find: ");
int missed = 0;
for (int v : data) {
if (broken(data, v) == -1) {
System.out.print(v + " ");
missed++;
}
}
System.out.println();
System.out.println(missed + " of " + data.length + " missed");
}
public static int broken(int[] a, int target) {
int low = 0;
int high = a.length - 1;
while (low < high) {
int mid = (low + high) / 2;
if (a[mid] == target) {
return mid;
}
if (a[mid] > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
}
values the broken search cannot find: 3 12 23 31 42 55 68 80
8 of 15 missed
Every value it misses sits at an even index: 0, 2, 4 and so on up to 14. Those are exactly the elements that binary search on fifteen items reaches last, when the range has shrunk to a single index and low equals high. The broken condition declares the loop over at that moment, so the one element left is never examined. Tried on 38, the first middle, or on 61, found on the second look, the broken version works perfectly, which is how this bug survives a quick test. The cure is to ask what a range of one element means. low equals high, and there is one place still to check, so the loop must still run: the condition is low <= high, for the same reason the recursive version's base case is low > high and not low >= high.
Searching Strings with compareTo
Nothing in the algorithm needs the elements to be numbers. It only needs a way to ask whether the middle element comes before or after the target, and for Strings that is compareTo from Lesson 3: negative if this String comes first, zero if the two are equal, positive if it comes later. Here is the recursive version searching a class roll in alphabetical order:
public class Names {
private static int looks = 0;
public static void main(String[] args) {
String[] roll = {"Ada", "Bruno", "Chidi", "Dara", "Emeka", "Fatima", "Gus", "Hana", "Ivo"};
System.out.print("roll: ");
for (String s : roll) {
System.out.print(s + " ");
}
System.out.println();
System.out.println();
report(roll, "Gus");
report(roll, "Ada");
report(roll, "Fergus");
}
public static void report(String[] a, String target) {
System.out.println("searching for " + target + ":");
looks = 0;
int where = find(a, target, 0, a.length - 1);
System.out.println(" result " + where + " after " + looks + " looks");
}
public static int find(String[] a, String target, int low, int high) {
if (low > high) {
return -1;
}
int mid = (low + high) / 2;
looks++;
int sign = a[mid].compareTo(target);
System.out.println(" low " + low + ", high " + high + ", mid " + mid + ", a[mid] is "
+ a[mid] + ", compareTo gives " + sign);
if (sign == 0) {
return mid;
}
if (sign > 0) {
return find(a, target, low, mid - 1);
}
return find(a, target, mid + 1, high);
}
}
roll: Ada Bruno Chidi Dara Emeka Fatima Gus Hana Ivo
searching for Gus:
low 0, high 8, mid 4, a[mid] is Emeka, compareTo gives -2
low 5, high 8, mid 6, a[mid] is Gus, compareTo gives 0
result 6 after 2 looks
searching for Ada:
low 0, high 8, mid 4, a[mid] is Emeka, compareTo gives 4
low 0, high 3, mid 1, a[mid] is Bruno, compareTo gives 1
low 0, high 0, mid 0, a[mid] is Ada, compareTo gives 0
result 0 after 3 looks
searching for Fergus:
low 0, high 8, mid 4, a[mid] is Emeka, compareTo gives -1
low 5, high 8, mid 6, a[mid] is Gus, compareTo gives 1
low 5, high 5, mid 5, a[mid] is Fatima, compareTo gives -4
result -1 after 3 looks
The sign does the job that < and > did for ints. Emeka against Gus gives -2: negative, so Emeka comes first and Gus must lie to its right, and low moves to 5. Emeka against Ada gives 4: positive, so Ada lies to the left, and high moves to 3. The sizes of those numbers are the distances between the first letters that differ, E and G being two apart and E and A four, which is exactly what the String documentation says compareTo returns. The search never uses the size. It uses only the sign, which is why the method stores the result once in sign and tests it against zero.
The search for Fergus is the not-found case again. After Emeka and Gus the range is the single index 5, Fatima. Fatima against Fergus gives -4, because at the second letter a comes four places before e, so low moves to 6, past high, and the method returns -1 after three looks.
A bug that hid for twenty years
One more detail, because it brings back the overflow from Lesson 2. On 2 June 2006, Joshua Bloch, who wrote the binary search in Java's own library, reported that it contained a bug, and that so did the binary search Jon Bentley had proved correct in his book Programming Pearls twenty years earlier. The bug is in the line you have been reading all lesson: int mid = (low + high) / 2;. For an array of about a billion elements or more, low + high can exceed the largest int and wrap around to a negative number.
public class Midpoint {
public static void main(String[] args) {
int low = 1500000000;
int high = 2000000000;
System.out.println("(low + high) / 2 = " + (low + high) / 2);
System.out.println("low + (high - low) / 2 = " + (low + (high - low) / 2));
}
}
(low + high) / 2 = -397483648
low + (high - low) / 2 = 1750000000
The second line is the repair Bloch suggested: compute the distance between the two ends first, which cannot overflow when both are valid indexes, and add half of it to low. No array in an AP question comes anywhere near that size, so (low + high) / 2 is correct there and is the form you should expect to read. The story is worth knowing for a different reason: code that had been proved correct ran for two decades before anyone's data was big enough to break it, and what broke it was the int range from the second lesson of this course.
What the exam asks you to do with it
Topic 4.14 covers linear search, which the framework describes as checking each element in order until the desired value is found or every element has been checked, starting from either end. Topic 4.17 covers binary search, and its learning objective is worded as a tracing task: determine the result of each iteration of a binary search. Search algorithms other than these two are outside the scope of the course and exam. Expect three kinds of question:
- Which elements are examined, and in what order, when searching for a given value. Answer with a table of low, high and mid, one row per look, exactly like the traces printed above.
- How many times the loop body runs, or how many calls are made, for a given target or in the worst case. Count the rows of your table; for the worst case, count halvings.
- What must be true for the code to work. The data must be sorted, in the direction the comparisons assume, which is increasing order for every search in this lesson.
Two habits pay for themselves. Compute mid with integer division every time, so the middle of 8 and 9 is 8, not 8.5 and not 9. And after each look, write down the new low or high before anything else: nearly every tracing error is a range end set to mid instead of mid + 1 or mid - 1.
Common misconceptions
- "Binary search finds the first copy of a repeated value." It returns whichever copy it lands on first. Dups.java, whose main is below, uses the same
binarySearchandlinearas before:int[] marks = {2, 5, 5, 5, 5, 9, 12}; System.out.println("binary search for 5 returned " + binarySearch(marks, 5)); System.out.println("linear search for 5 returned " + linear(marks, 5));
The first middle, (0 + 6) / 2, is index 3, and it already holds a 5, so the search stops there. Java's documentation for its own binary search says plainly that when several elements equal the key, there is no guarantee which one will be found.binary search for 5 returned 3 linear search for 5 returned 1 - "Binary search works on any array; sorting just makes it faster." Sorting is what makes it correct. On the jumbled array it missed five of the seven values that were there.
- "When the range holds an even number of elements there is no middle, so the code needs a special case." Integer division picks the left of the two middle positions, and either would do: the search still discards everything on the wrong side of whichever it picks.
- "To be sure a value is absent, a search has to look everywhere." In sorted data it does not. The search for 999,999 was certain after twenty looks, because each look ruled out a whole half.
- "Binary search is always the better choice." It needs sorted data, and sorting costs at least as much as one linear search, since a sort must read every element at least once, which is already a linear search's worst case. In practice a sort costs far more, as the next two lessons count. Binary search pays off when the same sorted data is searched many times.
The takeaway
Binary search keeps two indexes, low and high, around the part of a sorted array where the target could still be. Each look examines the middle, returns its index on a match, and otherwise moves low to mid + 1 or high to mid - 1, discarding the middle and one half. It stops when it finds the target or when low passes high, which means the range is empty. The recursive version passes the two ends as parameters; the loop version updates them in place, runs while low <= high, and makes the same looks in the same order. The worst case grows by one look each time the data doubles: 4 looks for 15 elements and 20 for a million, against a million for linear search. Take away sorted order and it fails without any error message; drop the equals sign from the loop condition and it misses every element it would have reached last.
So what?: on the exam, a binary search question is a table to fill in. Write low, high and mid for every look, move a range end to mid plus or minus one, and stop when low passes high.
The next lesson turns to the price of all this: getting data into sorted order in the first place, with two sorts whose every comparison is counted.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topic 4.14 Searching Algorithms (4.14.A.1: linear search checks each element in order until the value is found or all elements have been checked, and can begin from either end) and topic 4.17 Recursive Searching and Sorting (4.17.B.1 to 4.17.B.3: data must be sorted; binary search starts at the middle and eliminates half in each recursive call; it is typically more efficient than linear search; it can be written iteratively or recursively; other search algorithms are outside the scope of the course and exam). apcentral.collegeboard.org
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class Arrays, the binarySearch methods (the array must be sorted before the call or the results are undefined, and if several elements equal the key there is no guarantee which one will be found). docs.oracle.com
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class String, method compareTo (negative, zero or positive, and where the two strings first differ, the difference of the two character values). docs.oracle.com
- Bloch, J. (2006, June 2). Extra, extra - read all about it: Nearly all binary searches and mergesorts are broken. Google Research Blog (the sum low + high overflows for arrays of about a billion elements, and low + ((high - low) / 2) is one repair). research.google
- Wikipedia contributors. (2026). Binary search (in the worst case, binary search makes floor(log2 n) + 1 iterations of its comparison loop). en.wikipedia.org
- Key terms
- Binary search
- A search of sorted data that examines the middle of the remaining range and discards the half that cannot hold the target.
- Linear search
- A search that checks each element in turn from one end; it works on data in any order.
- low and high
- The two indexes that mark the ends of the range where the target could still be.
- mid
- The index examined at each look, computed as (low + high) / 2 with integer division.
- Empty range
- The state in which low has passed high, so no index is left to check and the search returns -1.
- Precondition
- Something that must be true before code runs for it to work, such as an array being sorted before a binary search.
- Worst case
- The most work an algorithm can need for inputs of a given size; for binary search, floor(log2 n) + 1 looks.
- compareTo
- The String method whose negative, zero or positive result tells a search to go left, stop, or go right.
Selection sort and insertion sort, with their comparisons counted
- Trace selection sort and insertion sort pass by pass on a small array, and give the contents of the array after any pass.
- Explain the difference between an element swapped into its final position and one inserted into its correct position so far.
- Count the comparisons each sort makes on sorted, reversed and scrambled data, and describe how the count grows as the array gets longer.
- Recognise each sort from its code, and swap two array elements correctly with a temporary variable.
Five numbers, two sorts, two different bills
The five numbers 38, 12, 45, 7 and 21 went into two sorting methods, one after the other, and both produced 7, 12, 21, 38, 45. Their bills were different. Selection sort compared two elements 10 times and made 4 swaps. Insertion sort compared two elements 8 times and moved an element one place to the right 6 times. Here is the whole run as a program called Sorts.java printed it, pass by pass:
selection sort on 38 12 45 7 21
pass 1: smallest left is 7, now 7 12 45 38 21
pass 2: smallest left is 12, now 7 12 45 38 21
pass 3: smallest left is 21, now 7 12 21 38 45
pass 4: smallest left is 38, now 7 12 21 38 45
10 comparisons, 4 swaps
insertion sort on 38 12 45 7 21
pass 1: inserted 12 at index 0, now 12 38 45 7 21
pass 2: inserted 45 at index 2, now 12 38 45 7 21
pass 3: inserted 7 at index 0, now 7 12 38 45 21
pass 4: inserted 21 at index 2, now 7 12 21 38 45
8 comparisons, 6 shifts
Same input, same answer, different work, and different arrays in between: after two passes one method had 7 12 45 38 21 and the other had 12 38 45 7 21. This lesson sets the two methods side by side. They are the two iterative sorts the course framework names, and it describes each in one sentence worth learning exactly, because exam questions turn on the difference. Selection sort repeatedly selects the smallest element from the unsorted part of the list and swaps it into its correct and final position. Insertion sort inserts an element from the unsorted part into its correct but not necessarily final position in the sorted part, shifting elements of the sorted part to make room.
Selection sort: find the smallest, swap it into place
Think of the array as two regions: a sorted part at the front, empty to begin with, and an unsorted part behind it. Each pass of selection sort scans the whole unsorted part for its smallest element and swaps that element to the front of the unsorted part, so the sorted part grows by one.
public static void selectionSort(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < a.length; j++) {
comparisons++;
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
int temp = a[i];
a[i] = a[minIndex];
a[minIndex] = temp;
moves++;
System.out.println(" pass " + (i + 1) + ": smallest left is " + a[i] + ", now " + show(a));
}
}
Walk the four passes against the output.
Pass 1, i is 0. minIndex starts at 0, where 38 is. The inner loop compares 12 with 38 (smaller, so minIndex becomes 1), then 45 with 12 (no), then 7 with 12 (smaller, minIndex becomes 3), then 21 with 7 (no). Four comparisons. The swap exchanges index 0 and index 3, giving 7 12 45 38 21. The 7 is now in its final place: nothing that happens later can move it, because every later pass only touches positions from i onwards.
Pass 2, i is 1. The smallest of 12, 45, 38 and 21 is 12, already at index 1, so three comparisons find nothing smaller and the swap exchanges index 1 with itself. The array does not change, but the swap still happens and still counts: this version of the code does not check whether it is needed.
Pass 3, i is 2. 38 beats 45, then 21 beats 38. Two comparisons, and 21 swaps with 45 to give 7 12 21 38 45.
Pass 4, i is 3. One comparison, 45 against 38, and another swap of an element with itself.
There is no fifth pass. When the unsorted part is down to one element, that element is the largest and is already in the last place, which is why the outer loop stops at a.length - 1. The comparisons were 4 + 3 + 2 + 1 = 10, and notice what that sum does not depend on: the values. The inner loop always runs from i + 1 to the end, so selection sort makes the same number of comparisons on any five numbers in any order. For n elements it is (n - 1) + (n - 2) + ... + 1, which adds up to n(n - 1) / 2.
The three lines of the swap are not negotiable. BadSwap.java tries to exchange 38 and 12 without a temporary variable, and then with one:
public class BadSwap {
public static void main(String[] args) {
int[] a = {38, 12};
a[0] = a[1];
a[1] = a[0];
System.out.println("without temp: " + a[0] + " " + a[1]);
int[] b = {38, 12};
int temp = b[0];
b[0] = b[1];
b[1] = temp;
System.out.println("with temp: " + b[0] + " " + b[1]);
}
}
without temp: 12 12
with temp: 12 38
The first assignment overwrote 38 before anything had saved it, so the second assignment copied 12 back into the place it came from. temp exists to hold the one value that the first assignment is about to destroy.
Insertion sort: take the next one, slide it into the sorted part
Insertion sort also grows a sorted part at the front, but it does its work the other way round. Instead of hunting for the smallest remaining element, it takes whichever element comes next, calls it the key, and walks it leftwards through the sorted part, shifting each larger element one place to the right, until it meets an element that is not larger. Then it drops the key into the gap. When people sort a hand of playing cards one card at a time, most do something very like this.
public static void insertionSort(int[] a) {
for (int i = 1; i < a.length; i++) {
int key = a[i];
int j = i - 1;
while (j >= 0 && isGreater(a[j], key)) {
a[j + 1] = a[j];
moves++;
j--;
}
a[j + 1] = key;
System.out.println(" pass " + i + ": inserted " + key + " at index " + (j + 1) + ", now " + show(a));
}
}
public static boolean isGreater(int x, int y) {
comparisons++;
return x > y;
}
The helper method isGreater is only there to count, and where it sits matters. The while condition tests j >= 0 first, and because && short-circuits, as in Lesson 5, isGreater is never called once j has run off the front of the array. So the counter records exactly the comparisons between two elements, and the same short circuit is what stops a[-1] from ever being read.
Pass 1: the key is 12. 38 is greater, so 38 shifts to index 1 and j becomes -1, which ends the loop without another comparison. 12 goes into index 0: one comparison, one shift, and 12 38 45 7 21.
Pass 2: the key is 45. 38 is not greater, so the loop stops at once and 45 goes back where it was: one comparison, no shifts.
Pass 3: the key is 7. 45, 38 and 12 are all greater and all shift right, and j runs off the front: three comparisons, three shifts, and 7 12 38 45 21.
Pass 4: the key is 21. 45 and 38 shift; 12 is not greater, so the loop stops there and 21 lands at index 2: three comparisons, two shifts, and the array is sorted.
That is 1 + 1 + 3 + 3 = 8 comparisons, and unlike selection sort's 10, this total depends entirely on the data. Pass 2 cost one comparison because 45 was already bigger than everything before it; pass 3 cost three because 7 had to travel to the front. Now look at 12. Pass 1 put it at index 0, which was correct among the two elements seen so far, and pass 3 pushed it to index 1 to make room for 7. That is precisely the framework's "correct but not necessarily final".
What matters here: after pass k of selection sort, the first k positions hold the k smallest values in the whole array, finished. After pass k of insertion sort, the first k + 1 positions are in order among themselves, and any of them may still be moved by a smaller value arriving later.
The two side by side
| Selection sort | Insertion sort | |
|---|---|---|
| One pass | scans the unsorted part for its smallest element, then makes one swap | takes the next element and shifts larger sorted elements right until it fits |
| After pass k | first k positions final | first k + 1 positions sorted among themselves, not final |
| Comparisons, 38 12 45 7 21 | 10 | 8 |
| Comparisons, any 5 elements | always 10 | from 4 to 10, depending on the order |
| Data movement | n - 1 swaps, some of an element with itself | from none up to n(n - 1) / 2 shifts |
| Can a pass stop early? | no: it must check every remaining element | yes: it stops at the first element that is not larger |
| Recognise it by | a minIndex found by an inner for loop, then one swap | a key saved from a[i] and a while loop that shifts |
Read the table row by row and one pattern runs through it. Selection sort does the same work whatever it is given: it cannot stop a scan early, because the smallest element might be the last one it looks at. Insertion sort does work in proportion to how disordered the data is: a key that belongs where it already is costs one comparison. The row about data movement cuts the other way. Selection sort makes exactly one swap per pass, n - 1 in all, while insertion sort can shift the same element over and over, as 38 was shifted in passes 1, 3 and 4.
The last row is the one to use on the exam when you are handed unlabelled code. An inner loop that remembers the index of a minimum and a single swap after it is selection sort. A saved key and a loop that copies a[j] into a[j + 1] is insertion sort.
Counting at scale
Five numbers cannot show how the costs grow. Counts.java runs both sorts, counting comparisons only, on arrays of 5, 10, 100 and 1,000 elements in three orders: already sorted, reversed, and scrambled by a formula so that every run sees the same numbers. The two sort methods are the ones above with the print lines and the move counter taken out; this is the rest of the program:
public class Counts {
private static int comparisons = 0;
public static void main(String[] args) {
int[] sizes = {5, 10, 100, 1000};
for (int n : sizes) {
System.out.println("n = " + n);
report("sorted ", sorted(n));
report("reversed ", reversed(n));
report("scrambled", scrambled(n));
}
}
public static void report(String label, int[] data) {
int[] copy = new int[data.length];
for (int i = 0; i < data.length; i++) {
copy[i] = data[i];
}
comparisons = 0;
selectionSort(data);
int selection = comparisons;
comparisons = 0;
insertionSort(copy);
int insertion = comparisons;
System.out.println(" " + label + " selection " + selection + ", insertion " + insertion);
}
public static int[] sorted(int n) {
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = i;
}
return a;
}
public static int[] reversed(int n) {
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = n - 1 - i;
}
return a;
}
public static int[] scrambled(int n) {
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = (i * 37 + 11) % n;
}
return a;
}
// selectionSort, insertionSort and isGreater as above, without printing or moves
}
n = 5
sorted selection 10, insertion 4
reversed selection 10, insertion 10
scrambled selection 10, insertion 6
n = 10
sorted selection 45, insertion 9
reversed selection 45, insertion 45
scrambled selection 45, insertion 31
n = 100
sorted selection 4950, insertion 99
reversed selection 4950, insertion 4950
scrambled selection 4950, insertion 2617
n = 1000
sorted selection 499500, insertion 999
reversed selection 499500, insertion 499500
scrambled selection 499500, insertion 245311
report copies the array before sorting it, because the first sort would otherwise hand the second an array that is already sorted: an array parameter refers to the caller's array, as Lesson 10 showed. With that taken care of, read the numbers.
- Selection sort's column never changes within a size. 10, 45, 4,950 and 499,500 are n(n - 1) / 2 for n = 5, 10, 100 and 1,000, whatever the order.
- Insertion sort ranges from n - 1 to the same n(n - 1) / 2. Sorted data costs one comparison per key, 999 for a thousand elements, because every key meets a smaller neighbour straight away. Reversed data is its worst case: every key travels to the front, and it ties with selection sort.
- On scrambled data insertion sort lands near half of selection sort's count: 2,617 of 4,950, and 245,311 of 499,500. On average a key travels about halfway through the sorted part before it fits.
- Both grow with the square of n. Ten times as many elements, from 100 to 1,000, means about a hundred times as many comparisons: 4,950 became 499,500.
That last line is the one that decides what these sorts are good for. Downey and Mayfield put a number on it in Think Java: selection sort's time is proportional to n squared, so sorting a million numbers this way needs on the order of a trillion steps. A thousand elements is fine; a million is not. Lesson 20's merge sort exists because of that row.
What the exam asks: the array after pass k
The framework's learning objective for this topic is a tracing task: determine the result of executing each step of a sorting algorithm. In practice that means questions of these kinds:
- Here is a sort and an array; what does the array contain after two (or three) iterations of the outer loop? Answer by writing the array out after every pass, as the program above printed it. Do not try to jump to pass three in your head.
- Which sort is this code? Use the last row of the table.
- How many times is a line such as
minIndex = j;ora[j + 1] = a[j];executed for a given array? Trace and tally, remembering that a comparison in a loop condition can run once more than the loop body it guards. - Which intermediate state is impossible for a given sort? After one pass of selection sort on 38 12 45 7 21, the smallest element, 7, must be at the front. After one pass of insertion sort, the first two elements are in order and nothing else has moved.
A useful check when you trace selection sort: after pass k, the first k values of your array must be the k smallest values of the original, in order. If they are not, you have made a mistake, and you have found it before it spread. For insertion sort the check is that the first k + 1 values are in order and are exactly the first k + 1 values of the original array, rearranged.
Common misconceptions
- "Selection sort is quicker on data that is already sorted." It cannot tell. Sorted or reversed, a thousand elements cost it 499,500 comparisons, because each scan has to reach the end to be sure it has found the smallest.
- "Insertion sort puts each element into its final position." Into its correct position among the elements seen so far. 12 was placed at index 0 in pass 1 and moved to index 1 in pass 3.
- "A swap is two assignments." It is three. Without a temporary variable the first assignment destroys a value, as BadSwap.java printed 12 12.
- "Insertion sort always makes fewer comparisons than selection sort." On reversed data they tie exactly, 499,500 each for a thousand elements. Insertion sort is never worse on comparisons, but it can be equal.
- "Comparisons and swaps are the same count." Selection sort made 10 comparisons and 4 swaps on five numbers; insertion sort made 8 comparisons and 6 shifts. Questions ask about one or the other, and they are different lines of code.
What you now know
Selection sort and insertion sort both build a sorted region at the front of an array, one pass at a time. Selection sort scans the unsorted region for its smallest element and swaps it into place, so each pass finishes one position for good; it always makes n(n - 1) / 2 comparisons and at most n - 1 swaps. Insertion sort takes the next element as a key and shifts larger elements right until the key fits, so each pass leaves a correctly ordered prefix that may still change; its comparisons run from n - 1 on sorted data to n(n - 1) / 2 on reversed data, with scrambled data near half the maximum. Both grow with the square of n. A swap needs a temporary variable, and the two sorts can be told apart by minIndex and one swap, or by a saved key and a shifting while loop.
The core of it: selection sort's work is fixed by the length of the array; insertion sort's work is set by how far out of order the data is.
A thousand scrambled elements cost insertion sort about a quarter of a million comparisons and selection sort half a million. The next lesson's sort does the same job in under eight thousand, and it does it by calling itself.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topic 4.15 Sorting Algorithms (4.15.A.1 to 4.15.A.3: selection sort and insertion sort are iterative sorts for an array or ArrayList; selection sort swaps the smallest unsorted element into its correct and final position; insertion sort inserts an element into its correct but not necessarily final position by shifting elements of the sorted portion). apcentral.collegeboard.org
- Wikipedia contributors. (2026). Selection sort (none of the loops depend on the data, and the comparisons total (n - 1) + (n - 2) + ... + 1 = n(n - 1) / 2). en.wikipedia.org
- Wikipedia contributors. (2026). Insertion sort (the best case is an already sorted array, taking linear time; the simplest worst case is an array in reverse order; the average case is quadratic; people sorting a bridge hand mostly use a similar method). en.wikipedia.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 13, sections 13.3 Selection Sort and 13.4 Merge Sort (selection sort's time is proportional to n squared; a million numbers would need about a trillion steps). O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Selection sort
- A sort that repeatedly finds the smallest element of the unsorted part and swaps it into its final position.
- Insertion sort
- A sort that takes the next element and shifts larger sorted elements right until it fits in the sorted part.
- Pass
- One iteration of a sort's outer loop, after which the sorted part of the array is one element longer.
- Swap
- Exchanging two elements with three assignments and a temporary variable that saves the first value.
- Shift
- Copying an element one place to the right, as in a[j + 1] = a[j], to open a gap for the key.
- Key
- In insertion sort, the element being placed on the current pass, saved before the shifting overwrites its slot.
- Comparison count
- The number of times two elements are compared; n(n - 1) / 2 for selection sort whatever the order of the data.
- Quadratic growth
- Work proportional to the square of n, so ten times the data costs about a hundred times as much.
Merge sort: the recursion that beats the loop
- Trace merge sort on an array of eight elements, giving every split and the contents of each range after every merge.
- Carry out the merge step by hand, repeatedly comparing the front elements of two sorted halves and copying the smaller.
- Count merge sort's comparisons and calls, and compare its growth with insertion sort's on scrambled data and on data already in order.
- Explain why merge needs both halves already sorted and why the method uses a temporary array.
249,124,319 comparisons against 341,097
A program called Race.java scrambled the numbers from 0 to 31,999 with a formula and sorted them twice, once with the insertion sort from Lesson 19 and once with a method called merge sort, counting comparisons between elements as before. Here are its figures for scrambled arrays of several sizes:
scrambled data
n = 8: insertion 17, merge 16
n = 1000: insertion 245311, merge 7951
n = 2000: insertion 976316, merge 17130
n = 4000: insertion 3896319, merge 36469
n = 8000: insertion 15572320, merge 77135
n = 16000: insertion 62292321, merge 162455
n = 32000: insertion 249124319, merge 341097
At eight elements there is nothing in it: 17 comparisons against 16. At 32,000, insertion sort made nearly a quarter of a billion comparisons and merge sort made 341,097, about 730 times fewer. Lesson 17 promised a recursion that does no wasted work and wins outright. This is it. Merge sort was invented by John von Neumann in 1945, and it is the third and last sort the course framework names. The framework calls it a recursive sorting algorithm and describes it in one sentence: merge sort repeatedly divides an array into smaller subarrays until each subarray is one element, and then recursively merges the sorted subarrays back together in sorted order to form the final sorted array.
This lesson walks one array of eight numbers through the whole process, every split and every merge, then runs the same program on data that is already in order, where the result of the race turns round.
The one idea: merging two sorted piles
Everything rests on a job that is easy: combining two lists that are each already sorted. Say the left half of an array holds 7 12 38 45 and the right half holds 3 21 29 60. The smallest element overall must be at the front of one half or the other, so compare the two fronts, 7 and 3, and take the smaller. Then compare the new fronts and take the smaller again:
| Left front | Right front | Taken | Result so far |
|---|---|---|---|
| 7 | 3 | 3 (right) | 3 |
| 7 | 21 | 7 (left) | 3 7 |
| 12 | 21 | 12 (left) | 3 7 12 |
| 38 | 21 | 21 (right) | 3 7 12 21 |
| 38 | 29 | 29 (right) | 3 7 12 21 29 |
| 38 | 60 | 38 (left) | 3 7 12 21 29 38 |
| 45 | 60 | 45 (left) | 3 7 12 21 29 38 45 |
| none left | 60 | 60, no comparison needed | 3 7 12 21 29 38 45 60 |
Seven comparisons placed eight elements. Each comparison puts exactly one element into its place, and once one half is empty, whatever remains of the other half is copied across without any comparison at all. So merging two halves with n elements between them costs at most n - 1 comparisons. Here is that step in Java, working on one array whose left half runs from from to mid and whose right half runs from mid + 1 to to:
public static void merge(int[] a, int from, int mid, int to, int[] temp) {
int i = from;
int j = mid + 1;
int k = from;
while (i <= mid && j <= to) {
comparisons++;
if (a[i] <= a[j]) {
temp[k] = a[i];
i++;
} else {
temp[k] = a[j];
j++;
}
k++;
}
while (i <= mid) {
temp[k] = a[i];
i++;
k++;
}
while (j <= to) {
temp[k] = a[j];
j++;
k++;
}
for (k = from; k <= to; k++) {
a[k] = temp[k];
}
}
Three indexes do the work. i is the front of the left half, j the front of the right half, and k the next free place in temp. The first loop runs while both halves still have elements, and each trip copies the smaller front into temp and moves on. When it ends, exactly one of the next two loops has anything to do: it copies the leftover elements of whichever half was not used up. The final for loop copies the merged range back into a. The merged values cannot be written straight into a, because they would overwrite elements of the left half that have not been taken yet; temp is the scratch space that prevents that.
The whole sort, traced
Merging needs two sorted halves. Where do they come from? From merge sort itself, applied to each half, and that is the recursion. A range of one element is already sorted, which is the base case. Anything longer is split at its middle, each half is sorted by a recursive call, and the two sorted halves are merged. The rest of MergeTrace.java adds print lines so you can watch:
public class MergeTrace {
private static int comparisons = 0;
public static void main(String[] args) {
int[] a = {38, 12, 45, 7, 21, 60, 3, 29};
trace(a);
System.out.println();
int[] b = {3, 7, 12, 21, 29, 38, 45, 60};
trace(b);
}
public static void trace(int[] a) {
comparisons = 0;
System.out.println("start " + show(a, 0, a.length - 1));
mergeSort(a);
System.out.println("done " + show(a, 0, a.length - 1) + " after " + comparisons + " comparisons");
}
public static void mergeSort(int[] a) {
int[] temp = new int[a.length];
sortRange(a, 0, a.length - 1, temp, 0);
}
public static void sortRange(int[] a, int from, int to, int[] temp, int depth) {
if (from >= to) {
return;
}
String pad = "";
for (int i = 0; i < depth; i++) {
pad += " ";
}
int mid = (from + to) / 2;
System.out.println(pad + "split " + show(a, from, to) + " into "
+ show(a, from, mid) + " and " + show(a, mid + 1, to));
sortRange(a, from, mid, temp, depth + 1);
sortRange(a, mid + 1, to, temp, depth + 1);
int before = comparisons;
merge(a, from, mid, to, temp);
System.out.println(pad + "merged " + show(a, from, to) + ", comparisons " + (comparisons - before));
}
public static String show(int[] a, int from, int to) {
String s = "[";
for (int i = from; i <= to; i++) {
s += a[i];
if (i < to) {
s += " ";
}
}
return s + "]";
}
// merge, exactly as printed above
}
The first half of its output, for 38 12 45 7 21 60 3 29:
start [38 12 45 7 21 60 3 29]
split [38 12 45 7 21 60 3 29] into [38 12 45 7] and [21 60 3 29]
split [38 12 45 7] into [38 12] and [45 7]
split [38 12] into [38] and [12]
merged [12 38], comparisons 1
split [45 7] into [45] and [7]
merged [7 45], comparisons 1
merged [7 12 38 45], comparisons 3
split [21 60 3 29] into [21 60] and [3 29]
split [21 60] into [21] and [60]
merged [21 60], comparisons 1
split [3 29] into [3] and [29]
merged [3 29], comparisons 1
merged [3 21 29 60], comparisons 3
merged [3 7 12 21 29 38 45 60], comparisons 7
done [3 7 12 21 29 38 45 60] after 17 comparisons
Read it in the order it happened, because the order is what the exam asks about.
- The whole range, indexes 0 to 7, splits at
mid= 3. Nothing moves. A split only computes an index. - The call for the left half, 0 to 3, splits again, and the call for 0 to 1 splits into two ranges of one element. Those two calls hit the base case and return at once, which is why they print nothing.
- Now the first merge happens, deep in the bottom left: 38 and 12 become 12 38. It took one comparison.
- Only now is 45 7 split, and merged into 7 45. Then the call for 0 to 3 merges its two sorted halves into 7 12 38 45, with three comparisons.
- Only after the entire left half is finished does the right half get split at all. It goes through the same steps and becomes 3 21 29 60.
- The top-level call merges the two sorted halves, which is the seven-comparison merge from the table above.
The indentation is the call stack from Lesson 17 made visible: each level of indentation is one level deeper in the recursion. Every call does its two recursive calls in order, left then right, and does its own merge only after both have returned, so the work happens on the way back up. That is the same shape as the factorial trace, where each multiplication waited for the answer from below.
The total was 1 + 1 + 3 + 1 + 1 + 3 + 7 = 17 comparisons. Selection sort on eight elements always needs 8 * 7 / 2 = 28.
Remember: in merge sort the array changes only in merge. A split moves nothing, and a range of one element is sorted by definition.
Why it wins: counting levels
Look at the trace again by level. At the bottom level there were four merges of one element with one element. At the next, two merges of two with two. At the top, one merge of four with four. Every level of merging handles all eight elements once, and a merge costs at most one comparison per element placed, so each level costs at most n comparisons. The number of levels is the number of times 8 can be halved before the pieces are single elements: three. That is the whole analysis, and it is the halving from Lesson 18 again: n comparisons per level times about log2 n levels. For a million elements that is about twenty million steps, where a sort that grows with n squared needs about a trillion, which is the comparison Downey and Mayfield give in Think Java.
Race.java checks that on real numbers. Its insertionSort and isGreater are the ones from Lesson 19, and its mergeSort, sortRange and merge are this lesson's without the print lines or the depth parameter. The new part is:
public static void main(String[] args) {
System.out.println("scrambled data");
int[] sizes = {8, 1000, 2000, 4000, 8000, 16000, 32000};
for (int n : sizes) {
race(n, scrambled(n));
}
System.out.println();
System.out.println("data that is already sorted");
race(1000, sorted(1000));
race(32000, sorted(32000));
}
public static void race(int n, int[] data) {
int[] copy = new int[n];
for (int i = 0; i < n; i++) {
copy[i] = data[i];
}
comparisons = 0;
insertionSort(data);
int insertion = comparisons;
comparisons = 0;
mergeSort(copy);
System.out.println(" n = " + n + ": insertion " + insertion + ", merge " + comparisons);
}
with scrambled and sorted exactly as in Lesson 19's Counts.java. Go back to the scrambled figures at the top of the lesson and read them in pairs. Each time n doubles, insertion sort's count roughly quadruples: 62,292,321 became 249,124,319. Merge sort's count slightly more than doubles: 162,455 became 341,097. Doubling the data adds one more level of merging and doubles the work at every level, and that small extra beyond doubling is the one extra level. Keep doubling and the gap keeps widening, which is why at 32,000 elements it is a factor of 730.
This is also where merge sort differs from the Fibonacci recursion in Lesson 17. fib(n - 1) and fib(n - 2) recomputed the same values separately. The two calls in sortRange work on halves that do not overlap, so no element is ever compared twice at the same level. Recursion is expensive when it repeats work, and merge sort never does.
The same program on data already in order
Now change the input and nothing else. The second half of MergeTrace's output is the same eight numbers, already sorted:
start [3 7 12 21 29 38 45 60]
split [3 7 12 21 29 38 45 60] into [3 7 12 21] and [29 38 45 60]
split [3 7 12 21] into [3 7] and [12 21]
split [3 7] into [3] and [7]
merged [3 7], comparisons 1
split [12 21] into [12] and [21]
merged [12 21], comparisons 1
merged [3 7 12 21], comparisons 2
split [29 38 45 60] into [29 38] and [45 60]
split [29 38] into [29] and [38]
merged [29 38], comparisons 1
split [45 60] into [45] and [60]
merged [45 60], comparisons 1
merged [29 38 45 60], comparisons 2
merged [3 7 12 21 29 38 45 60], comparisons 4
done [3 7 12 21 29 38 45 60] after 12 comparisons
Every split and every merge still happened, in exactly the same order. What changed is the cost of each merge. When every element of the left half is smaller than every element of the right half, the left half runs out after one comparison per element, and the right half is then copied across with no comparisons at all. The top merge cost 4 instead of 7, and the total fell from 17 to 12.
That is a saving, but a modest one, and here is the flip. The last lines of Race.java's output ran both sorts on data that was already sorted:
data that is already sorted
n = 1000: insertion 999, merge 5044
n = 32000: insertion 31999, merge 241408
On sorted data insertion sort wins, by a factor of five at a thousand elements and more than seven at 32,000. Insertion sort's work depends on how disordered the data is, and here there is no disorder, so each key costs one comparison. Merge sort cannot notice that the data is already in order: it splits all the way down and merges all the way back up whatever it is given. Neither sort is simply the faster one. Merge sort guarantees about n log2 n comparisons on any input; insertion sort ranges from n - 1 to about n2 / 2, and which of the two is cheaper depends on the data.
The upshot: merge sort's advantage is a guarantee. Its worst case is about as good as its best case, and on large scrambled data that guarantee is worth a factor of hundreds.
What merge needs, and what merge sort costs
merge is not a sort. It relies completely on both halves arriving sorted. BadMerge.java calls it by itself on 38 12 45 7, a left half of 38 12 and a right half of 45 7, neither of them sorted:
public static void main(String[] args) {
int[] a = {38, 12, 45, 7};
int[] temp = new int[a.length];
merge(a, 0, 1, 3, temp);
System.out.println("merge alone on 38 12 | 45 7 gives " + a[0] + " " + a[1] + " " + a[2] + " " + a[3]);
}
merge alone on 38 12 | 45 7 gives 38 12 45 7
Nothing moved. 38 was smaller than 45, so it was taken; then 12 was smaller than 45, so it was taken, and the left half was used up; then 45 and 7 were copied across in the order they were in. Every step followed the rule, and the rule is only correct when each half is in order. In merge sort that is guaranteed by the recursion: by the time a call reaches its merge, both of its recursive calls have returned with their halves sorted, and the base case guarantees it for ranges of one.
The price of merge sort is the temp array. Selection and insertion sort rearrange the array they are given and need only a variable or two besides; merge sort needs a second array as long as the first. That is why mergeSort creates temp once, before the recursion starts, and passes it down, rather than having every call create its own.
What the exam asks you to do with it
The learning objective is again a tracing task: determine the result of each iteration of the merge sort algorithm. Sorting algorithms other than selection, insertion and merge sort are outside the scope of the course and exam, and so, under topic 4.16, is writing recursive code yourself. So expect to be handed a merge sort like the one above and asked things like these:
- What does the array contain after a particular call to
merge? Draw the split tree, do the whole left branch before touching the right, and remember that only merges change the array. - How many times is
merge, or the recursive method, called for an array of a given length? The activity below finds the rule. - Which two ranges are merged last, and what are they at that moment? Always the two sorted halves of the whole array.
- What happens if the base case is removed, or the split computes a middle that does not shrink the range? The recursion never stops, and you get Lesson 17's
StackOverflowError.
Common misconceptions
- "Merge sort sorts as it splits." Splitting only computes
mid. The first three lines of the trace are all splits and the array is still 38 12 45 7 21 60 3 29 after them. Every change happens inmerge. - "merge can combine any two halves into sorted order." Only sorted ones. On 38 12 and 45 7 it returned 38 12 45 7, unchanged.
- "Both halves are sorted at the same time." One call runs at a time. The left half, 38 12 45 7, was completely sorted before the right half was split even once.
- "Merge sort is faster than insertion sort on every input." On data that is already sorted, insertion sort made 999 comparisons for a thousand elements and merge sort 5,044.
- "Merge sort sorts the array in place like the other two." It needs a temporary array as long as the one being sorted, and every merged range is copied back from it.
Summing up
Merge sort sorts a range by splitting it at the middle, sorting each half with a recursive call, and merging the two sorted halves; a range of one element is the base case. The merge compares the front elements of the two halves, copies the smaller into a temporary array, copies whatever is left once one half runs out, and copies the merged range back. Traced on eight elements, the splits go down the left branch first, the first merge happens at the bottom left, the right half is untouched until the left half is finished, and the top-level merge comes last. Each level of merging costs at most n comparisons and there are about log2 n levels, so merge sort grows like n log2 n: 341,097 comparisons for 32,000 scrambled elements against insertion sort's 249,124,319. On data already in order the positions reverse, because insertion sort's work depends on the disorder and merge sort's does not.
In short: split without moving anything, merge with one comparison per element placed, and do the left branch completely before the right.
That completes the algorithms of the course. The last module turns to the two things that decide how the exam goes: reading what Java tells you when a program is wrong, and writing answers the way the exam is scored.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 4, topic 4.17 Recursive Searching and Sorting (4.17.C.1 and 4.17.C.2: merge sort is a recursive sorting algorithm for an array or ArrayList that repeatedly divides the array until each subarray is one element and then recursively merges the sorted subarrays; sorts other than selection, insertion and merge sort are outside the scope of the course and exam) and topic 4.16 (writing recursive code is outside the scope). apcentral.collegeboard.org
- Wikipedia contributors. (2026). Merge sort (invented by John von Neumann in 1945; a list of one element is considered sorted; average performance proportional to n log n; the usual array version needs auxiliary space proportional to n). en.wikipedia.org
- Wikipedia contributors. (2026). Divide-and-conquer algorithm (an algorithm that recursively breaks a problem into subproblems of the same kind until they are simple enough to solve directly, then combines their solutions). en.wikipedia.org
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Chapter 13, sections 13.4 to 13.7 on merge sort (time proportional to n log2 n; for a million numbers, about 20 million steps against a trillion for selection sort). O'Reilly Media. Free online edition at greenteapress.com
- Key terms
- Merge sort
- A recursive sort that splits a range in half, sorts each half recursively, and merges the two sorted halves.
- Merge
- Combining two sorted runs into one by repeatedly copying the smaller of their front elements.
- Divide and conquer
- Solving a problem by splitting it into smaller problems of the same kind and combining their answers.
- Split
- Computing the middle of a range so it can be sorted as two halves; no element moves during a split.
- Temporary array
- The scratch array merge writes into before copying back, so that unread elements are not overwritten.
- Level
- One layer of the split tree; each level of merging handles every element once, at most n comparisons.
- n log n growth
- Work proportional to n times log2 n: about twenty million steps for a million elements, not a trillion.
Module 8: Debugging and the Exam
Four lessons that point everything at the exam. The first reads what Java says when a program is wrong, from real broken programs: compiler messages whose line numbers are late, stack traces that start inside the library, a file that is not there, and the silent wrong answer that no message reports. The second sets out what the AP Computer Science A exam contains, how long each section runs and how the free-response answers are scored. The last two are practice: four original free-response questions in the exam's format, each with a model solution written the way the scoring guidelines reward, compiled and run.
Reading compiler errors and stack traces
- Read a stack trace from the top down: name the exception, use its message, skip the library frames and find the first line of your own code.
- Explain why a compiler error can be reported on a later line than the real mistake, and fix errors one at a time from the first.
- Tell syntax errors, run-time errors and logic errors apart, and name the exception a given mistake produces.
- Read numbers from a text file with File and Scanner, and recognise the compile-time and run-time errors that file reading adds.
Line 427 of a file you never wrote
Report.java prints a summary of each class period's test scores, including the middle score in the list. It worked for Period 2 and then stopped:
import java.util.ArrayList;
public class Report {
public static void main(String[] args) {
ArrayList<Integer> scores = new ArrayList<Integer>();
scores.add(71);
scores.add(88);
scores.add(64);
summary("Period 2", scores);
ArrayList<Integer> none = new ArrayList<Integer>();
summary("Period 5", none);
}
public static void summary(String label, ArrayList<Integer> list) {
System.out.println(label + ": " + list.size() + " scores, middle one " + middle(list));
}
public static int middle(ArrayList<Integer> list) {
return list.get(list.size() / 2);
}
}
Period 2: 3 scores, middle one 88
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:100)
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106)
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302)
at java.base/java.util.Objects.checkIndex(Objects.java:385)
at java.base/java.util.ArrayList.get(ArrayList.java:427)
at Report.middle(Report.java:19)
at Report.summary(Report.java:15)
at Report.main(Report.java:11)
Here is a reading that feels natural and is wrong. The first line after the exception names a file called Preconditions.java, and the fifth names line 427 of ArrayList.java, so something inside Java's list class must have broken. You did not write either file, and there is nothing in them for you to fix. Follow that reading and you spend twenty minutes looking for a bug in the library, which is not where the problem is.
The trace is not a list of suspects. It is the call stack from Lesson 17 at the moment things went wrong, printed newest call first. Read from the bottom up, it is the story of what happened: main, at line 11, called summary; summary, at line 15, called middle; middle, at line 19, called ArrayList.get; and get called a chain of checking methods inside the library, the last of which found the index was illegal and threw the exception. The library lines are the library doing its job, which is refusing a bad request. The question is who made the request, and the answer is the first line that names your own file: Report.middle, line 19.
Line 19 is return list.get(list.size() / 2);. Now use the message, which is the most informative thing on the screen: Index 0 out of bounds for length 0. The list had length 0, so it was empty, and 0 / 2 is 0, so the method asked an empty list for its element at index 0. Walk one frame further down and you find where the empty list came from: main at line 11, summary("Period 5", none). The code is wrong in a precise way. middle has no sensible answer for an empty list, and nothing stops it being given one. The fix is a decision, not a typo: check list.size() > 0 in summary before asking for a middle, or state as a precondition that the list is never empty and make sure main honours it.
Why this matters: the course framework calls this a run-time error, a mistake that shows up while the program is running and typically makes it terminate abnormally, and it calls the particular kind here an exception: an unexpected error the compiler did not detect, which interrupts the normal flow of the program. The compiler could not have caught it. Whether a list is empty is not known until the program runs.
Reading any stack trace in four steps
Oracle's tutorial describes what happens when a method throws an exception: the runtime system searches back through the call stack, starting with the method where the error occurred and going through the callers in the reverse order they were called, looking for code that handles it. No method in this course handles exceptions, so the search reaches the bottom of the stack and the program terminates, printing the stack as it goes. That gives a fixed routine for reading one:
- Read the first line to the end. The exception's class name says what kind of thing went wrong, and the message after the colon often says exactly what: the bad index, the length, the input that could not be read.
- Skip the lines that begin with java.base. They are inside the Java library.
- Stop at the first line that names your file. That line of your code made the request that failed.
- Read downwards from there to see who called that method with what, until you find where the bad value came from.
Try it on a second program. Parse.java adds up a line of comma-separated numbers using split, the String method from the Java Quick Reference, and Integer.parseInt:
public class Parse {
public static void main(String[] args) {
String line = "14,9,twelve,30";
String[] parts = line.split(",");
int total = 0;
for (String p : parts) {
total += Integer.parseInt(p);
System.out.println("running total " + total);
}
}
}
running total 14
running total 23
Exception in thread "main" java.lang.NumberFormatException: For input string: "twelve"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.parseInt(Integer.java:778)
at Parse.main(Parse.java:7)
Step 1: NumberFormatException, for the input string "twelve". Step 2: skip three library lines. Step 3: Parse.main, line 7, the parseInt call. Step 4 is not needed, because main called nobody. And the two lines of output before the trace are evidence too: the loop ran twice successfully before it failed, so the first two parts were fine and the third was the problem. Output that appears before a crash is a record of how far the program got. parseInt turns "14" into 14; given a word, it has no number to return, and throwing is its only honest option.
Compiler errors: the message, the caret, and the line that is really wrong
A compiler error is a different animal. The program never runs at all. The framework's word for the commonest kind is a syntax error, a mistake where the rules of the language are not followed, which the compiler detects, and errors the compiler detects must be fixed before the program can be run. Every javac message has the same shape: the file and line number, the word error and a description, a copy of the offending line, and a caret, ^, under the place where the compiler gave up. The programs below were compiled with javac, and each message is exactly what it printed.
When the message is exactly right. Lengths.java mixes up the two ways of asking how big something is:
import java.util.ArrayList;
public class Lengths {
public static void main(String[] args) {
int[] scores = {71, 88, 64};
ArrayList<String> names = new ArrayList<String>();
names.add("Ines");
System.out.println(scores.size());
System.out.println(names.length);
}
}
Lengths.java:8: error: cannot find symbol
System.out.println(scores.size());
^
symbol: method size()
location: variable scores of type int[]
Lengths.java:9: error: cannot find symbol
System.out.println(names.length);
^
symbol: variable length
location: variable names of type ArrayList<String>
2 errors
Cannot find symbol means a name the compiler has no record of in that place, and the two extra lines say which name and where it looked. An array has a length field and no methods; an ArrayList has a size() method and no length; a String has a length() method. The same message comes from a misspelled variable or method name. A related one comes from a value of the wrong type:
ArrayList<String> ages = new ArrayList<String>();
ages.add("16");
ages.add("17");
int first = ages.get(0);
Ages.java:8: error: incompatible types: String cannot be converted to int
int first = ages.get(0);
^
1 error
The list holds Strings, and the String "16" is not the int 16, however much it looks like one. Integer.parseInt(ages.get(0)) is the conversion, and Parse.java showed what it does with a String that is not a number.
When the line number is late. The compiler reports an error where it first becomes certain something is wrong, and that can be after the mistake. Grade.java:
public class Grade {
public static void main(String[] args) {
int score = 72;
if (score >= 60); {
System.out.println("pass");
} else {
System.out.println("fail");
}
}
}
Grade.java:6: error: 'else' without 'if'
} else {
^
1 error
Line 6 is fine. The mistake is the semicolon at the end of line 4, which ends the if statement right there with an empty body; the braces on lines 4 to 6 are then an ordinary block that always runs, and when the compiler reaches else there is no if for it to belong to. Lesson 5 met the same stray semicolon from the other side, when it compiled silently and printed the wrong thing. Brace.java is later still:
public class Brace {
public static void main(String[] args) {
int count = 0;
for (int i = 1; i <= 10; i++) {
if (i % 3 == 0) {
count++;
}
System.out.println(count);
}
}
Brace.java:10: error: reached end of file while parsing
}
^
1 error
The indentation says the brace on line 7 closes the for loop. The compiler does not read indentation. It pairs braces by counting, so line 7 closes the if, line 9 closes the for, line 10 closes main, and the class is never closed at all; the compiler only finds out when the file runs out. The missing brace belongs after line 6, and the message points at line 10. For a message about end of file, or about a brace or an else, look above the line reported.
When the compiler is protecting you. Two errors are not about spelling at all. They are the compiler proving that something could go wrong on some run:
public static int firstNegative(int[] a) {
for (int i = 0; i < a.length; i++) {
if (a[i] < 0) {
return i;
}
}
}
Negatives.java:13: error: missing return statement
}
^
1 error
int[] values = {4, 8, 15};
int total;
for (int v : values) {
total += v;
}
System.out.println(total);
Total.java:6: error: variable total might not have been initialized
total += v;
^
Total.java:8: error: variable total might not have been initialized
System.out.println(total);
^
2 errors
In the first, an array with no negative numbers would run the loop to the end and fall out of the method without returning anything, so a return -1; after the loop is required, exactly the linear search shape from Lesson 10. In the second, total += v means total = total + v, which reads total before anything was ever put in it. int total = 0; is the fix, and the reason accumulators in this course always start with a value.
One rule covers all of these. Downey and Mayfield put it plainly in Think Java: only the first error message is truly reliable, so fix one error at a time and recompile. A single mistake can produce several messages, and fixing the first often makes the rest vanish, or, as the activity shows, reveals errors the compiler had not yet reached.
Reading a file, and the errors that come with it
The course framework includes reading data from a text file, using the File and Scanner classes listed on the Java Quick Reference, and file reading brings errors of its own, so this is the place to meet it. A file called temps.txt holds four temperatures on one line:
18 21 17 19
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
public class Temps {
public static void main(String[] args) throws IOException {
Scanner in = new Scanner(new File("temps.txt"));
int total = 0;
int count = 0;
while (in.hasNext()) {
int t = in.nextInt();
total += t;
count++;
}
in.close();
System.out.println(count + " readings, total " + total);
}
}
4 readings, total 75
Line by line: new File("temps.txt") names the file; new Scanner(...) opens it for reading; hasNext() is true while there is another item to read, which makes it the condition of a while loop; nextInt() reads the next item as an int; and close() closes the file when the program has finished with it. The import lines are required because File and IOException live in the java.io package and Scanner in java.util. That leaves the words throws IOException in the header of main, and the quickest way to see why they are there is to delete them:
Temps.java:6: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner in = new Scanner(new File("temps.txt"));
^
1 error
Opening a file can fail for reasons no compiler can check in advance, so Java insists that code which might fail this way says what should happen if it does. Oracle's tutorial calls this the Catch or Specify Requirement: the code must either handle the exception or declare that its method can throw it. FileNotFoundException is one kind of IOException, and the framework's way of meeting the requirement is the declaration, throws IOException in the method header, with the consequence it states: if the file cannot be opened, the program terminates. Here is that happening, run with temps.txt deleted:
Exception in thread "main" java.io.FileNotFoundException: temps.txt (The system cannot find the file specified)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.util.Scanner.<init>(Scanner.java:645)
at Temps.main(Temps.java:7)
The same four steps: a missing file, four library frames, then line 7, where the Scanner was created. The text in brackets comes from the operating system, and this run was on Windows; on a Mac or Linux machine it reads differently. <init> is how a stack trace names a constructor. Finally, put the file back with a word where a number should be:
18 21 seventeen 19
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:947)
at java.base/java.util.Scanner.next(Scanner.java:1602)
at java.base/java.util.Scanner.nextInt(Scanner.java:2267)
at java.base/java.util.Scanner.nextInt(Scanner.java:2221)
at Temps.main(Temps.java:11)
This one has no message after its name, so step 1 gives only the class, and the class is enough: the Scanner documentation says nextInt throws InputMismatchException when the next token is not a valid int. Line 11 is the nextInt call, and hasNext() was perfectly happy, because "seventeen" is an item; it just is not an int.
The error that prints nothing
The hardest errors are the ones Java cannot see at all. Average.java:
public class Average {
public static void main(String[] args) {
int[] two = {90, 80};
int[] none = new int[0];
System.out.println("average of 90 and 80: " + average(two));
System.out.println("average of no scores: " + average(none));
}
public static double average(int[] a) {
int sum = 0;
for (int v : a) {
sum += v;
}
return (double) sum / a.length;
}
}
average of 90 and 80: 85.0
average of no scores: NaN
No exception, no message, and an answer that is not a number. With ints, dividing by zero throws the ArithmeticException you met in Lesson 5. Because the cast makes this a double division, 0.0 divided by 0 is quietly NaN, which stands for "not a number", and the program carries on. The framework calls this a logic error: a mistake in the algorithm that makes the program behave incorrectly, found only by testing it with specific data and checking the result. Nothing in Java will point at it. The defence is the habit this course has used throughout: test the empty case, the one-element case and the ends, and check at least one answer by hand. When a result is wrong and there is no message, print the values in the middle of the computation, as the traces in Lessons 17 to 20 did, and find the first one that disagrees with your hand calculation.
Bottom line: a compiler error tells you the program is not legal Java; an exception tells you it did something impossible while running; a logic error tells you nothing, which is why you test.
What the exam asks about errors
The multiple-choice section asks you to be the compiler and the virtual machine. Expect code segments with a question such as "what is printed, or does an error occur?", where one option names an exception. The exceptions the framework mentions by name are the ones you have now seen: ArithmeticException, NullPointerException, StringIndexOutOfBoundsException, ArrayIndexOutOfBoundsException, IndexOutOfBoundsException for an ArrayList, ConcurrentModificationException from Lesson 11, and InputMismatchException from a Scanner. Expect, too, to be asked which kind of error a mistake is: syntax, run-time or logic.
The free-response section is the opposite situation. You write code by hand with no compiler to complain, and the scoring guidelines deliberately forgive certain small slips that a compiler would reject, such as confusing length and size, which Lesson 22 lists. That forgiveness covers typing, not thinking: a loop that reads past the end of an array still loses the point for accessing the elements without bounds errors.
Common misconceptions
- "The mistake is on the line the compiler names." Often it is, but for a stray semicolon or a missing brace the report comes later: line 6 for Grade.java's semicolon on line 4, and line 10 for Brace.java's missing brace after line 6.
- "The top line of a stack trace is where the bug is." The top lines are usually inside the Java library. The first line naming your own file is where your code made the request that failed.
- "Fix every error from the first compile, then recompile once." Only the first message is fully reliable. Fix it, compile again, and read the new first message.
- "If it runs without an exception, it is correct." Average.java ran cleanly and printed NaN.
- "An exception means the compiler missed something it should have caught." The compiler cannot know that a list will be empty or that a file will contain the word seventeen. Those facts exist only when the program runs.
Looking back
Java reports trouble in three different ways. A compiler error stops the program before it starts; its message names a file, a line and a caret, and for missing punctuation the real mistake can sit several lines above the one reported, so fix the first error, recompile, and repeat. An exception stops a running program and prints the call stack newest first: read the class and message, skip the java.base lines, stop at the first line of your own file, and read downwards to find where the bad value came from. A logic error prints nothing and is found only by testing with data whose answers you know. Reading a file with File and Scanner adds a requirement, throws IOException in the header, and two exceptions, FileNotFoundException for a missing file and InputMismatchException for an item of the wrong type.
The next lesson leaves the keyboard behind and looks at the exam itself: how long it is, what each section asks, and how a reader scores what you write.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Unit 1, topic 1.1, essential knowledge 1.1.B.2 and 1.1.C.1 to 1.1.C.4 (errors the compiler detects must be fixed before a program can run; syntax, logic and run-time errors; an exception is a run-time error the compiler did not detect), and Unit 4, topic 4.6 Using Text Files (File and Scanner; add throws IOException to the header of a method that uses a file, and if the file name is invalid the program terminates; nextInt throws InputMismatchException when the next int does not exist). apcentral.collegeboard.org
- Oracle. (n.d.). The Java Tutorials: What Is an Exception? (the runtime system searches the call stack in the reverse order the methods were called, and if no handler is found the program terminates). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: The Catch or Specify Requirement (code that might throw certain exceptions must be enclosed in a try that handles them or in a method that specifies it can throw them; runtime exceptions are not subject to the requirement). docs.oracle.com
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class Scanner (nextInt throws InputMismatchException if the next token does not match the Integer regular expression or is out of range). docs.oracle.com
- Downey, A. B., and Mayfield, C. (2020). Think Java: How to Think Like a Computer Scientist (2nd ed.), Appendix D: Debugging (only the first compiler error message is truly reliable, so fix one error at a time; a stack trace lists the method that was running and the methods that called it; logic errors produce no message). O'Reilly Media. greenteapress.com
- Key terms
- Syntax error
- A mistake that breaks the rules of the language, detected by the compiler before the program can run.
- Run-time error
- A mistake that shows up while the program runs and usually makes it terminate abnormally.
- Exception
- A run-time error the compiler did not detect, which interrupts the normal flow of the program.
- Logic error
- A mistake in the algorithm that gives wrong results without any message, found only by testing.
- Stack trace
- The list of calls in progress when an exception was thrown, printed newest first, one frame per line.
- Caret
- The ^ under a line in a compiler message, marking where the compiler gave up, which may be after the real mistake.
- throws IOException
- A clause in a method header declaring that the method may fail while opening or reading a file.
- InputMismatchException
- The exception Scanner's nextInt throws when the next item in the input is not a valid int.
What the AP Computer Science A exam actually asks
- Describe the current exam: two ninety-minute sections, 42 multiple-choice questions worth 55 percent and four free-response questions worth 45 percent.
- Name the four free-response question types, their point values and what each asks you to write.
- Explain how free-response answers are scored, including algorithm points and the errors readers are told to ignore.
- Plan the three hours: pacing, tracing technique, and the habits the scoring guidelines reward.
Three hours, forty-six questions
The AP Computer Science A exam lasts three hours and contains forty-six questions: forty-two multiple-choice questions in the first ninety minutes and four free-response questions in the second ninety. The whole exam is taken on a screen, in the College Board's Bluebook testing application, and every response is submitted automatically when time runs out. Here is the format as the 2025 course and exam description sets it out:
| Section | Questions | Time | Share of the score | Time per question |
|---|---|---|---|---|
| I: Multiple choice | 42 | 90 minutes | 55% | a little over 2 minutes |
| II: Free response | 4, worth 25 points | 90 minutes | 45% | about 22 minutes |
If you practise from older books or question banks, you will see a different description. From 2015 the exam gave 40 multiple-choice questions in its ninety minutes. The current version, the one in the 2025 framework, adds two questions and weights the multiple-choice section at 55 percent. Older material also asks you to write things the current exam does not, such as subclasses, as Lesson 14 explained. When a practice question disagrees with this lesson about the format, trust the course and exam description.
This lesson follows the three hours in order, and stops at each point where knowing how the exam works changes what you should do.
Before the first question: Bluebook and the Quick Reference
The Java Quick Reference, the list of library methods that may appear on the exam, is built into Bluebook, and schools can also print it and hand it out on exam day. Every class on it has appeared in this course: String (including split), Integer (MIN_VALUE, MAX_VALUE, parseInt), Double.parseDouble, Math (abs, pow, sqrt, random), ArrayList (size, both add methods, get, set, remove), File and Scanner for reading a file, which Lesson 21 introduced, and the equals and toString that every Object has. The sheet tells you what a method is called and what it returns. It does not tell you when to use it, which is the part the exam tests.
One sentence in the course description changes how you should practise. Free-response answers are typed into a code editor window, and that editor does not compile the code and gives no feedback about it. There is no red underline and no error message: whatever you type is what the reader sees. The same page recommends practising with autocompletion turned off, because students who lean on it all year can struggle to write code without it. So for the last weeks before the exam, write some of your practice answers in a plain text editor, and only then compile them to see what you missed. Bluebook also offers test previews for the digital AP exams, which are the way to see the real screens before the day.
The first ninety minutes: 42 questions
The multiple-choice section is mostly single questions, with occasionally one or two sets of two questions each. The College Board's student page describes what you will be doing: working out which code segment produces a given output, identifying the output, value or result of given code from its starting values, and analysing code for correctness, equivalence and errors. All four units of the course are assessed:
| Unit | Share of the multiple-choice section |
|---|---|
| 1: Using Objects and Methods | 15% to 25% |
| 2: Selection and Iteration | 25% to 35% |
| 3: Class Creation | 10% to 18% |
| 4: Data Collections | 30% to 40% |
The framework also weights its five computational thinking practices, and one number stands out. Practice 3, Analyze Code, which it defines as determining the output or result of given code or explaining why code may not work as intended, carries 37 to 53 percent of the multiple-choice section, more than any other. Practice 2, Develop Code, carries 22 to 38; Practice 4, describing behaviour and the conditions that produce a result, 10 to 15; and Design Code and Use Computers Responsibly 2 to 10 each. In plain terms, around half of this section is tracing. That is why every program in this course was printed with its real output under it, and why Lessons 17 to 20 printed the state of every variable at every step: the skill being measured is predicting that output before you see it.
Ninety minutes for 42 questions is about two minutes and eight seconds each. A question about a nested loop or a recursion can easily take four minutes to trace properly, so the time has to come from somewhere, and it comes from the questions that ask about a single expression or a single method call. How the section is scored matters here: the College Board's student page says the multiple-choice score is the total number of correct responses. There is no deduction for a wrong answer, so a blank earns exactly what a wrong guess earns, nothing. Answer every question, and mark the long traces to come back to rather than letting one of them eat five minutes early on.
The point: the multiple-choice section rewards the tracing habits of this course: write the variables down, update them one statement at a time, and do not trust your intuition about what a loop does.
The second ninety minutes: four questions, 25 points
Every free-response question assesses the same practice, Develop Code: you write Java. Each has a fixed job:
| Question | Points | What you write |
|---|---|---|
| 1: Methods and Control Structures | 7 | two methods, or a constructor and a method, of a class you are given. Part A (4 points) needs loops or conditionals, or both, and calls to methods in that class; Part B (3 points) needs calls to String methods |
| 2: Class Design | 7 | a whole class from a scenario and a table showing how it is used and what it returns: the class header, instance variables, a constructor, and a method |
| 3: Data Analysis with ArrayList | 5 | one method that uses, analyses or changes data held in an ArrayList |
| 4: 2D Array | 6 | one method that uses, analyses or changes data held in a 2D array |
The questions are not worth the same. Ninety minutes for 25 points is 3.6 minutes a point, which suggests roughly 25 minutes each for Questions 1 and 2, 18 for Question 3 and 22 for Question 4. A question worth 5 points does not deserve half an hour.
The wording of the questions follows a small vocabulary that the course description lists. Assume means take something as true without checking it: if a question says to assume an array has at least one element, you do not need to handle an empty one, and code that checks anyway is wasted time. Complete means some of the code is already written and you fill in the rest. Implement or write means produce the code. Questions also give method headers, and you must use them as given; the next section shows what it costs not to.
How a reader scores what you typed
No computer runs your free-response answers. They are scored at the annual AP Reading, held in the first two weeks of June, by college professors and experienced AP teachers working from a set of scoring guidelines written for each question. The guidelines list criteria, each worth one point, so a 5-point question has five things the reader looks for. The course description explains the rules the readers apply, and they are worth knowing precisely.
Algorithm points. Each question has one or two criteria marked as algorithm points. They ask whether all the necessary steps of a solution are present in an appropriate order. An algorithm point can often still be earned when a step assessed by a different criterion is present but wrong. It is not earned when required pieces are missing or put together in the wrong order, when extra code makes the solution incorrect (printing output, or a wrong check of a precondition, are the examples given), when a response confuses array access with collection access in a way the question does not assess elsewhere, when persistent data is changed (such as the object a parameter refers to), when a void method or constructor returns a value, or when a provided method header is rewritten with a different number or type of parameters.
Other points have a narrow focus, such as calling a particular method or comparing two particular values, and can be earned even when there are errors elsewhere in the response.
Errors to ignore. For every point, some mistakes count as minor and do not cost the point. Among them:
| The slip | An example |
|---|---|
| length and size confused, with or without brackets | words.size() for an array |
| = and == swapped | if (count = 3) |
| a missing semicolon or brace, where the structure or indentation makes the intent clear | a statement with no semicolon at the end of its line |
| square, round and angle brackets confused | list[i] written for a get |
| a spelling or capitalisation slip, where the meaning is unambiguous | ArayList |
| a local variable used without being declared | count++ with no int count |
| maths symbols used for operators | a written less-than-or-equal sign for <= |
| a provided method header rewritten with different variable names | int min renamed int limit |
The spelling allowance has a limit that the guidelines spell out: it applies only when the correct name can be inferred without doubt. If you declare two variables whose names differ only by capital letters and then use the wrong one, the reader cannot assume which you meant. And the confusion of [] with get in that table is forgiven as a matter of syntax, while the algorithm rule above can still apply when a response mixes up array and ArrayList access in a way the question cares about.
Here is how those rules play out on one small, original question in the exam's style: write countAtLeast(String[] words, int min), which returns how many words have a length of at least min. A rubric written the College Board's way might have four points: (1) accesses every element of words with no bounds errors; (2) calls length() on an element and compares it with min correctly for "at least"; (3) counts the qualifying words, initialising and incrementing a count inside the loop; (4) returns the correct count (algorithm). Now three responses, each compiled and run on the words loop, array, if, recursion and class with min set to 5.
Response A confuses length and size twice:
public static int countAtLeast(String[] words, int min) {
int count = 0;
for (int i = 0; i < words.size(); i++) {
if (words[i].length >= min) {
count++;
}
}
return count;
}
Words.java:9: error: cannot find symbol
for (int i = 0; i < words.size(); i++) {
^
symbol: method size()
location: variable words of type String[]
Words.java:10: error: cannot find symbol
if (words[i].length >= min) {
^
symbol: variable length
location: class String
2 errors
The compiler rejects it outright. A reader does not: both slips are length and size confusion, which is on the list to ignore, and with them ignored the logic is complete and correct. Under this rubric it earns all four points. That is not permission to be careless, because the list is short and specific, but it is a reason not to spend exam time agonising over whether it was length or length().
Response B compiles, and runs off the end of the array:
for (int i = 0; i <= words.length; i++) {
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at Words.countAtLeast(Words.java:10)
at Words.main(Words.java:4)
This is not a slip on the list. It is a real error in the logic, and it costs the point for accessing every element without bounds errors. Whether it costs anything more depends on the decision rules of the particular question; the College Board's own examples often let the algorithm point survive an error that another criterion has already charged for.
Response C is correct and chatty:
if (words[i].length() >= min) {
count++;
System.out.println("found " + words[i]);
}
found array
found recursion
found class
3
It returns 3, which is right, and it will probably lose the algorithm point anyway, because the method was asked to return a count and this one also prints. Printing to output is the course description's first example of extra code that makes a solution incorrect. Methods on this exam return what they are asked to return and print nothing unless told to.
The correct version is Response A with length and size put right. Tested on three values of min, 5, 3 and 10, it printed:
3
4
0
Remember: readers forgive a short list of typing slips and nothing else. Bounds, logic, extra printing and rewritten headers all cost points, whether or not the code would compile.
From raw points to a 5
Your free-response points are weighted and combined with your multiple-choice score into a composite score, and the composite is converted to an AP score from 1 to 5. The course description stresses that the exams are not graded on a curve: every student who meets the standard for a score receives it, however many that is. The student site describes the conversion as a statistical process meant to make, for example, a 3 this year mean the same level of achievement as a 3 last year. The 5-point score is a recommendation about how well qualified you are for college credit and placement, and each college decides for itself which scores it will accept for what.
One consequence of the weights is worth working out. If the stated shares were applied directly, each multiple-choice question would carry 55 / 42, about 1.3 percent of the exam, and each free-response point 45 / 25, or 1.8 percent. The official conversion is more involved than that, but the arithmetic makes the point: a single free-response point is worth more than a single multiple-choice question, and the easiest free-response points, such as a correct method header or a loop that visits every element, are the cheapest marks on the exam.
A plan for the three hours
- Multiple choice: about two minutes a question. Trace with a table of variables, not in your head. Answer everything, since a wrong answer costs nothing more than a blank, and come back to long traces at the end.
- Before writing any free-response code: read the whole question, including the examples. Note every method you are given, because Question 1's Part A expects you to call methods of the class, and a question can make that a condition: the sample question in the course description says full credit requires using a provided method appropriately. Rewriting what a provided method already does wastes time and can cost that point.
- Copy the header exactly. Same name, same parameter types in the same order, same return type. Renaming a parameter is forgiven; changing the number or type of parameters costs the algorithm point.
- Return, do not print. Unless the question asks for output, a method that prints is doing extra work that can make it wrong.
- Leave parameters alone. Copy a String or read an array; do not change the data a parameter refers to unless the question asks you to.
- Check your loop bounds against the examples. The worked examples in the question are test cases. Trace your code on one of them before you move on; it is the only testing you will get.
- Keep going. Each criterion is scored on its own, and non-algorithm points can be earned despite errors elsewhere in the response, so a partial answer still collects points. A Part B can be answered even if Part A defeated you.
The next two lessons put this plan to work on four original questions written in the exam's format, one of each type, each with a model solution, a rubric in the College Board's style, and the output of the solution compiled and run.
Common misconceptions
- "The exam has 40 multiple-choice questions." That was the earlier exam. The 2025 framework's exam has 42, worth 55 percent of the score.
- "Free-response code that would not compile scores nothing." Readers score it against criteria, and ignore a published list of slips, including length and size confusion and a missing semicolon where the intent is clear. Response A would not compile and still earned full marks.
- "The four free-response questions are worth the same." They are worth 7, 7, 5 and 6 points.
- "I must memorise the String and ArrayList method signatures." They are on the Quick Reference in Bluebook. What you must know is when to use each, which the sheet does not tell you.
- "A correct answer is correct however I get there." A method that prints as well as returning, or that rewrites the header it was given, can lose the algorithm point even when the value it returns is right.
Recap
The current exam is three hours in Bluebook: 42 multiple-choice questions in 90 minutes for 55 percent of the score, then four free-response questions in 90 minutes for 45 percent. The multiple-choice section covers all four units and is about half tracing; its score is the number of correct answers, so answer everything. The free-response questions are Methods and Control Structures (7 points, with a String part), Class Design (7), Data Analysis with ArrayList (5) and 2D Array (6), typed into an editor that does not compile. Readers score them against one-point criteria, including one or two algorithm points that are lost to missing steps, extra printing, changed parameters or rewritten headers, and they ignore a short list of typing slips. The composite of both sections becomes a score from 1 to 5.
Key idea: the exam rewards two things this course has practised on every page: predicting exactly what code does, and writing code that does exactly what was asked, no more.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description: Exam Information (exam overview: 3 hours, 42 multiple-choice questions for 55 percent and 4 free-response questions for 45 percent, 90 minutes each; question types and points 7, 7, 5 and 6, with Part A of Question 1 worth 4 and Part B 3; unit and practice weightings; task verbs), Scoring Guidelines: Applying the Scoring Criteria (algorithm points, non-algorithm points, errors to ignore), Instructional Approaches (free-response code editor does not compile or give feedback; turn off autocompletion), the AP Program overview (free-response answers scored at the AP Reading, composite converted to a 1 to 5 score, not graded on a curve), and Appendix: Java Quick Reference. apcentral.collegeboard.org
- College Board. (n.d.). AP Computer Science A: The Exam. AP Central (a fully digital exam in Bluebook with responses submitted automatically; the Java Quick Reference is included in Bluebook and schools may print it; the multiple-choice section is mostly individual questions with occasional sets of two; test previews in Bluebook). Accessed September 2026. apcentral.collegeboard.org
- College Board. (n.d.). AP Computer Science A: Assessment. AP Students (exam duration 3 hours; what the multiple-choice questions ask you to do; the four free-response question types). Accessed September 2026. apstudents.collegeboard.org
- College Board. (n.d.). About AP Scores. AP Students (the multiple-choice score is the total number of correct responses; free-response answers are scored at the AP Reading in the first two weeks of June; composite scores are converted to the 5-point scale so that a score means the same from year to year; colleges make their own credit decisions). Accessed September 2026. apstudents.collegeboard.org
- Wikipedia contributors. (2026). AP Computer Science A (background on earlier exam formats, including 40 multiple-choice questions in 90 minutes from 2015, and the later change to 42 questions weighted at 55 percent). en.wikipedia.org
- Key terms
- Bluebook
- The College Board's testing application, in which the whole AP Computer Science A exam is taken.
- Java Quick Reference
- The list of library classes and methods that may appear on the exam, provided in Bluebook.
- Free-response question
- A question answered by writing Java code, scored by readers against one-point criteria.
- Scoring guidelines
- The criteria for one free-response question, each worth one point, with decision rules for borderline answers.
- Algorithm point
- A point for having all the necessary steps in an appropriate order, lost to missing steps, extra printing or a rewritten header.
- Errors to ignore
- The published list of minor slips, such as length and size confusion, that do not cost a point.
- AP Reading
- The annual scoring session in June at which college faculty and AP teachers score free-response answers.
- Composite score
- The weighted combination of both sections, converted into the final AP score from 1 to 5.
Free-response practice 1: methods and control structures, and a class
- Read a free-response question the way the exam lays it out: the class skeleton, preconditions, examples and the general directions.
- Write model answers to an original Question 1 (a loop that calls a provided method, and a String method) and an original Question 2 (a class designed from a table of examples).
- Score responses against a rubric in the College Board's style, including algorithm points and the instance-variable rules.
- Test a written answer against the question's own examples, and use a changed input to expose an answer that only looks right.
The page of directions before Question 1
Section II of the 2026 exam opened with a page of directions, and they are the same kind of fine print this lesson's questions assume. All code must be Java. The classes on the Java Quick Reference count as already imported. Unless a question says otherwise, you may assume parameters are not null and that methods are only called when their preconditions are satisfied. You may use any accessible method of the classes defined in a question, and writing a lot of code that a call to one of those methods could replace will not receive full credit. Scratch paper is allowed, but only what you type into Bluebook is scored, and you can move back and forth between the four questions until time runs out.
A question then gives you a class with some parts filled in and some left for you, written in a fixed style. /* implementation not shown */ marks a method you may call but cannot see: trust its comment, which is its specification. /* to be implemented in part (a) */ marks your job. A comment beginning Precondition states something that is guaranteed to be true when the method is called, so you must not waste time checking it, and a line saying there may be other instance variables, constructors and methods means you should use only what is shown. What follows are two original questions written in that format, a rubric for each in the College Board's style, a model answer that was compiled and run, and the plausible wrong answers that the rubric exists to catch.
Question 1: TrailRecord
This question involves a hiking trail with numbered mile markers and the record of one hike along it. You will write two methods of the TrailRecord class.
public class TrailRecord {
/** The number of mile markers on the trail, numbered 0 to numMarkers - 1.
* numMarkers is at least 2. */
private int numMarkers;
/** The times the hike started and finished, each in the form "h:mm" or
* "hh:mm" on a 24-hour clock, such as "8:05" or "14:30". The finish is
* later on the same day than the start. */
private String startTime;
private String finishTime;
/** Returns the elevation, in feet, at mile marker m.
* Precondition: 0 <= m < numMarkers */
public int getElevation(int m) {
/* implementation not shown */
}
/** Returns the total climb along the trail, as described in part (a). */
public int totalClimb() {
/* to be implemented in part (a) */
}
/** Returns the length of the hike in minutes, as described in part (b). */
public int hikeMinutes() {
/* to be implemented in part (b) */
}
// There may be instance variables, constructors, and methods that are not shown.
}
(a) Write the method totalClimb. Walking from marker 0 to the last marker, each step from one marker to the next either climbs, stays level, or descends. The total climb is the sum of the climbs only: for each pair of consecutive markers where the elevation increases, add the increase; level and downhill steps add nothing. For example, if the elevations at markers 0 to 5 are 1200, 1350, 1300, 1500, 1500 and 1450, the steps are +150, -50, +200, 0 and -50, and totalClimb returns 350.
(b) Write the method hikeMinutes, which returns the number of minutes from startTime to finishTime. For example, a hike from "8:05" to "14:30" lasted 385 minutes, and a hike from "9:50" to "10:10" lasted 20 minutes.
Part (a), built from the specification
Start with what you are given, not with a blank loop. There is a method that answers the elevation at any marker, a count of markers, and a precondition that says which markers exist. The quantity asked for is built from pairs of neighbours, so the natural loop visits each marker that has a neighbour behind it, from 1 to numMarkers - 1, and compares it with m - 1. Every call then satisfies the precondition automatically: the smallest index used is 0 and the largest is numMarkers - 1.
public int totalClimb() {
int climb = 0;
for (int m = 1; m < numMarkers; m++) {
int change = getElevation(m) - getElevation(m - 1);
if (change > 0) {
climb += change;
}
}
return climb;
}
A rubric in the College Board's style for this part would carry four points:
| Point | Criterion |
|---|---|
| 1 | Loops over the pairs of consecutive markers, with every call to getElevation inside its precondition (no bounds errors) |
| 2 | Calls getElevation for both markers of a pair |
| 3 | Identifies a climb: the later elevation is greater than the earlier one |
| 4 | Accumulates only the increases and returns the total (algorithm) |
To test it you need a working getElevation, which the question keeps hidden. The test version of the class stores the elevations in an array and has a constructor that takes the array and the two times; the two methods above are pasted in unchanged. This main method tries three trails:
public class Main {
public static void main(String[] args) {
int[] ridge = {1200, 1350, 1300, 1500, 1500, 1450};
TrailRecord hike = new TrailRecord(ridge, "8:05", "14:30");
System.out.println("total climb: " + hike.totalClimb());
System.out.println("hike minutes: " + hike.hikeMinutes());
int[] upOnly = {300, 420, 610, 700};
TrailRecord steady = new TrailRecord(upOnly, "9:50", "10:10");
System.out.println("total climb: " + steady.totalClimb());
System.out.println("hike minutes: " + steady.hikeMinutes());
int[] downOnly = {900, 700, 650};
TrailRecord descent = new TrailRecord(downOnly, "11:45", "12:05");
System.out.println("total climb: " + descent.totalClimb());
System.out.println("hike minutes: " + descent.hikeMinutes());
}
}
total climb: 350
hike minutes: 385
total climb: 400
hike minutes: 20
total climb: 0
hike minutes: 20
350 matches the question's example, which is the first thing to check. The steady climb gives 120 + 190 + 90 = 400, and a trail that only descends gives 0, which is right: it has no climbs to add.
Two answers to part (a) that look right
The first plausible answer uses Math.abs, perhaps from a feeling that elevation changes should be counted as distances:
int change = Math.abs(getElevation(m) - getElevation(m - 1));
total climb: 450
hike minutes: 385
total climb: 400
hike minutes: 20
total climb: 250
hike minutes: 20
Look at the middle trail first. On 300, 420, 610, 700 it returned 400, exactly like the model answer, because that trail never goes down and Math.abs changes nothing when every difference is already positive. Had that been the only test, the answer would have passed. The ridge trail, which does go down, exposes it: 450 instead of 350, because the two descents of 50 were added as if they were climbs, and the all-downhill trail reports 250 feet of climbing where there was none. This is the whole reason a question gives an example with a descent in it. Test on the example that contains the awkward case, not just the one that is easy to check. On a rubric like the one above, this answer loses point 3 and the algorithm point.
The second plausible answer counts pairs from the other end:
for (int m = 0; m < numMarkers; m++) {
int change = getElevation(m + 1) - getElevation(m);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 6 out of bounds for length 6
at TrailRecord.getElevation(TrailRecord.java:15)
at TrailRecord.totalClimb(TrailRecord.java:21)
at Main.main(Main.java:5)
On the last trip m is 5 and the call is getElevation(6), which breaks the precondition. In the test version the hidden method happens to use an array, so the violation shows up as an exception; on the exam nothing runs, and the reader simply withholds point 1. Pairing m with m + 1 is perfectly good if the loop stops one early, at m < numMarkers - 1. The two versions are equally correct; mixing them is what fails.
Worth holding on to: an answer that passes the easy example has not been tested. The example that includes the unusual case, a descent here, is the one that separates a correct answer from a nearly correct one.
Part (b): a String method
Part B of Question 1 is the one that requires String methods, and this one needs three from the Quick Reference: indexOf to find the colon, substring to cut out the hours and the minutes, and Integer.parseInt to turn each piece into a number. The same conversion is needed twice, once for each time, which is the signal to write a helper method. A helper is ordinary Java and is fine in a free-response answer, as long as the method the question asked for exists with exactly its given header.
public int hikeMinutes() {
return toMinutes(finishTime) - toMinutes(startTime);
}
private int toMinutes(String time) {
int colon = time.indexOf(":");
int hours = Integer.parseInt(time.substring(0, colon));
int minutes = Integer.parseInt(time.substring(colon + 1));
return hours * 60 + minutes;
}
Converting both times to minutes after midnight turns the subtraction into ordinary arithmetic: 14:30 is 870 minutes, 8:05 is 485, and 870 - 485 is the 385 printed above. Doing it any other way means borrowing an hour when the minutes of the finish are smaller than the minutes of the start, which is exactly the kind of case a rushed answer gets wrong. A three-point rubric might read:
| Point | Criterion |
|---|---|
| 5 | Locates the colon in a time String using indexOf |
| 6 | Extracts the hour and minute parts with substring and converts them with Integer.parseInt |
| 7 | Returns the correct number of minutes between the two times (algorithm) |
Here is the answer that seems to save a call:
int hours = Integer.parseInt(time.substring(0, 2));
int minutes = Integer.parseInt(time.substring(3));
total climb: 350
Exception in thread "main" java.lang.NumberFormatException: For input string: "8:"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at java.base/java.lang.Integer.parseInt(Integer.java:778)
at TrailRecord.toMinutes(TrailRecord.java:34)
at TrailRecord.hikeMinutes(TrailRecord.java:30)
at Main.main(Main.java:6)
It assumes every hour has two digits. For "14:30" that is true and the answer works; for "8:05", which the question's own example uses, substring(0, 2) is "8:", and parseInt refuses it, just as it refused "twelve" in Lesson 21. The question said times look like "h:mm" or "hh:mm", and that sentence is the reason indexOf is required: only by finding the colon can the code handle both lengths. This answer would lose points 5 and 7.
Question 2: StreakCounter
This question involves tracking a daily habit. You will write the complete StreakCounter class.
A StreakCounter is created with the name of a habit and a goal, the number of days in a row the user is aiming for. Each day the user calls record, passing true if the habit was done that day and false if it was not, and record returns a message. A day done adds one to the current streak; the message is the habit name followed by ": day ", the streak, " of " and the goal, unless the streak has reached the goal, when it is the habit name followed by ": goal met, streak " and the streak. A day missed ends the streak: the message is the habit name followed by ": streak of ", the length of the streak that ended, and " ended", and the streak goes back to 0; if there was no streak to end, the message is the habit name followed by ": no streak yet". The table shows the behaviour:
| Statement | Value returned | Explanation |
|---|---|---|
StreakCounter reading = new StreakCounter("reading", 3); | A counter for reading, aiming for 3 days in a row. | |
reading.record(true) | "reading: day 1 of 3" | The streak is now 1. |
reading.record(true) | "reading: day 2 of 3" | |
reading.record(false) | "reading: streak of 2 ended" | The streak returns to 0. |
reading.record(true) | "reading: day 1 of 3" | |
reading.record(true) | "reading: day 2 of 3" | |
reading.record(true) | "reading: goal met, streak 3" | The goal is reached. |
reading.record(true) | "reading: goal met, streak 4" | The streak keeps counting past the goal. |
reading.record(false) | "reading: streak of 4 ended" | |
StreakCounter piano = new StreakCounter("piano", 2); | A second, separate counter. | |
piano.record(false) | "piano: no streak yet" | There is no streak to end. |
piano.record(true) | "piano: day 1 of 2" | |
reading.record(true) | "reading: day 1 of 3" | Each counter keeps its own streak. |
piano.record(true) | "piano: goal met, streak 2" |
Write the complete StreakCounter class. Your implementation must meet all specifications and conform to the examples shown in the table.
Designing the class from the table
Question 2 is answered in three passes. First, read the table for the state: what must an object remember between calls? The habit name appears in every message, the goal appears in the "day" messages and decides when the goal is met, and the current streak changes from call to call. Three instance variables: a String and two ints, all private. Second, read the first row for the constructor: it takes a String and an int, in that order, and a new counter plainly starts with no streak. Third, read the remaining rows for the method: its name, its boolean parameter, its String return value, and the four cases its messages fall into.
public class StreakCounter {
private String habit;
private int goal;
private int streak;
public StreakCounter(String habitName, int daysWanted) {
habit = habitName;
goal = daysWanted;
streak = 0;
}
public String record(boolean done) {
if (!done) {
if (streak == 0) {
return habit + ": no streak yet";
}
String message = habit + ": streak of " + streak + " ended";
streak = 0;
return message;
}
streak++;
if (streak >= goal) {
return habit + ": goal met, streak " + streak;
}
return habit + ": day " + streak + " of " + goal;
}
}
One detail in the missed-day branch is easy to get backwards. The message must report the streak that ended, so it has to be built before streak is set to 0; reset first and every message would say "streak of 0 ended". The goal test is >= rather than == because the table shows the goal message continuing on day 4.
The table itself is the test. This main method makes exactly the calls in the table, in order:
public class Main {
public static void main(String[] args) {
StreakCounter reading = new StreakCounter("reading", 3);
System.out.println(reading.record(true));
System.out.println(reading.record(true));
System.out.println(reading.record(false));
System.out.println(reading.record(true));
System.out.println(reading.record(true));
System.out.println(reading.record(true));
System.out.println(reading.record(true));
System.out.println(reading.record(false));
StreakCounter piano = new StreakCounter("piano", 2);
System.out.println(piano.record(false));
System.out.println(piano.record(true));
System.out.println(reading.record(true));
System.out.println(piano.record(true));
}
}
reading: day 1 of 3
reading: day 2 of 3
reading: streak of 2 ended
reading: day 1 of 3
reading: day 2 of 3
reading: goal met, streak 3
reading: goal met, streak 4
reading: streak of 4 ended
piano: no streak yet
piano: day 1 of 2
reading: day 1 of 3
piano: goal met, streak 2
Every line matches the table's second column. A rubric in the College Board's style would give seven points:
| Point | Criterion |
|---|---|
| 1 | Declares the class header public class StreakCounter (not private) |
| 2 | Declares private instance variables of appropriate types for the habit, the goal and the current streak, none of them static |
| 3 | Declares the constructor header public StreakCounter(String ___, int ___) |
| 4 | The constructor gives every instance variable its correct initial value |
| 5 | Declares the method header public String record(boolean ___) |
| 6 | Updates the streak and builds the correct message in all four cases (algorithm) |
| 7 | Returns the constructed message in every case |
Four of those seven points are headers and declarations. They are the cheapest points on the exam, and they are only lost by not reading the table carefully: a constructor that takes its int before its String, or a method called recordDay, costs a point that the logic can never win back.
The answer that gives every habit the same streak
Point 2 contains a condition that looks fussy until you run it: none of the instance variables may be static. The College Board's own sample rubric for this question type withholds the instance-variable point from a response that declares any of them static. Here is why. Change one word:
private static int streak;
and run the same main method:
reading: day 1 of 3
reading: day 2 of 3
reading: streak of 2 ended
reading: day 1 of 3
reading: day 2 of 3
reading: goal met, streak 3
reading: goal met, streak 4
reading: streak of 4 ended
piano: no streak yet
piano: day 1 of 2
reading: day 2 of 3
piano: goal met, streak 3
The first ten lines are identical, because until the piano counter does anything, only one counter is being used. Then the damage appears. After piano's first day, reading reports day 2, though its own streak had ended, and piano reports a streak of 3 after two days of practice. A static variable belongs to the class, not to any object, so there was only ever one streak, and every counter was adding to it. Lesson 9 made this distinction; the table in the question tested for it deliberately, with its final three rows interleaving two counters. The row explained as "each counter keeps its own streak" is there for exactly this reason.
Common misconceptions
- "If my answer gives the example's result, it is right." The
Math.absanswer matched the model on the all-uphill trail and was still wrong. Check against the example that contains the awkward case. - "A precondition is something I should check with an if." A precondition is a promise to you. Checking it wastes time, and extra code that changes the result can cost the algorithm point.
- "I can assume times have two-digit hours." The question said "h:mm" or "hh:mm" and gave "8:05" as an example. Assumptions the question did not make are the commonest way to lose a String point.
- "Instance variables can be public, or static, if the program still works." The rubric requires private, non-static instance variables, and a static one does not even work once two objects exist.
- "Helper methods are not allowed." They are ordinary Java. What matters is that the method the question asks for exists with exactly the header given.
The short version
A free-response question gives you a class skeleton whose comments are specifications: call the methods whose implementation is not shown, trust the preconditions, and write only what is marked for you. For Question 1, build part (a) around the method you are given, looping over exactly the indexes the precondition allows, and use the String methods of the Quick Reference for part (b), finding delimiters with indexOf rather than assuming positions. For Question 2, read the table three times, for the state, for the constructor and for the method, and collect the header and declaration points that careful reading guarantees. Then test every answer against the question's own examples, starting with the one that contains the unusual case.
Why this matters: most lost free-response points are not lost to hard algorithms. They are lost to an assumption the question never made, a header copied wrongly, or an example that was never traced.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Exam Information (Question 1: Methods and Control Structures, 7 points, Part A 4 points with iteration or conditionals and calls to methods of the class, Part B 3 points calling String methods; Question 2: Class Design, 7 points, from a scenario and a table of examples, including a class header, instance variables, a constructor and a method) and Scoring Guidelines (applying the scoring criteria; the sample Class Design rubric withholds the instance-variable point from a response that declares any instance variable static). apcentral.collegeboard.org
- College Board. (2026). AP Computer Science A 2026 Free-Response Questions, Section II directions only (Quick Reference classes treated as imported; parameters not null and preconditions satisfied unless noted; use accessible methods of the classes in the question; only responses entered in the application are scored). apcentral.collegeboard.org
- Oracle. (n.d.). The Java Tutorials: Declaring Member Variables (a private field is accessible only within its own class, and in the spirit of encapsulation fields are commonly made private). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Understanding Class Members (each object has its own copy of an instance variable, while a static field is a single variable common to all objects of the class). docs.oracle.com
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class Integer, method parseInt (throws NumberFormatException if the String does not contain a parsable integer). docs.oracle.com
- Key terms
- Class skeleton
- The partly written class a free-response question gives you, with comments that specify each method.
- Implementation not shown
- A method you may call and must trust from its comment, though its code is hidden.
- Helper method
- An extra method you write to avoid repeating code, allowed as long as the required method keeps its header.
- Rubric point
- One criterion in a question's scoring guidelines, earned or not independently of most others.
- Header point
- A point for declaring a class, constructor or method header exactly as the question specifies.
- State
- What an object must remember between method calls, which becomes its instance variables.
- Static variable
- A variable shared by every object of a class; wrong for data each object must keep separately.
Free-response practice 2: array, ArrayList, and a 2D array
- Answer an original Question 3 that rearranges an ArrayList of objects, and an original Question 4 that analyses a 2D array, in the exam's format.
- Trace a plausible wrong answer until the exact step where it fails, and name the rubric point it loses.
- Remove elements from an ArrayList during a traversal without skipping, revisiting or reordering them.
- Traverse rows and columns of a 2D array with the correct bounds, and use a 1D array to hold one result per row.
A method that moved four books out of two
Five books sit on a reading list: Arrival, Beloved, Coraline, Dune and Emma, and two of them, Arrival and Coraline, are finished. A method was supposed to move the finished books to the end of the list and report how many it had moved. It left the list as Beloved, Dune, Emma, Coraline, Arrival and returned 4. That is the output of a plausible answer to the first question in this lesson, and nothing about its code looks careless. This lesson works the other way round from Lesson 23: for each question, it starts from answers that look right, traces each to the exact step where it goes wrong, names the point that step costs, and only then builds the model answer. As before, the questions are original, written in the exam's format, and every answer was compiled and run.
Question 3: ReadingLog
This question involves a reading list of books. You will write one method of the ReadingLog class.
public class Book {
/** Returns the title of this book */
public String getTitle() {
/* implementation not shown */
}
/** Returns true if this book has been finished, and false otherwise */
public boolean isFinished() {
/* implementation not shown */
}
// There may be instance variables, constructors, and methods that are not shown.
}
public class ReadingLog {
/** The books on the reading list, in order. Guaranteed not to be null. */
private ArrayList<Book> books;
/** Moves every finished book to the end of books and returns the
* number of finished books, as described below. */
public int moveFinishedToEnd() {
/* to be implemented */
}
// There may be instance variables, constructors, and methods that are not shown.
}
Write the method moveFinishedToEnd. After the method runs, books must contain the same books as before, with every unfinished book before every finished book. The unfinished books must keep their original order relative to each other, and so must the finished books. The method returns the number of finished books. For example, if books holds Arrival (finished), Beloved, Coraline (finished), Dune and Emma, then afterwards it holds Beloved, Dune, Emma, Arrival, Coraline, and the method returns 2.
To run answers, the test version of Book adds a constructor and a toString that marks a finished book with an asterisk, and a main method tries two lists: the example above, and Holes (finished), Ivanhoe (finished), Jaws.
The answer that returned 4
public int moveFinishedToEnd() {
int count = 0;
for (int i = 0; i < books.size(); i++) {
if (books.get(i).isFinished()) {
Book b = books.remove(i);
books.add(b);
count++;
}
}
return count;
}
before: [Arrival*, Beloved, Coraline*, Dune, Emma]
after: [Beloved, Dune, Emma, Coraline*, Arrival*], returned 4
before: [Holes*, Ivanhoe*, Jaws]
after: [Ivanhoe*, Jaws, Holes*], returned 2
Trace the first list with the index and the list side by side, because the fault is in how they move against each other.
| i | Book at i | Action | List afterwards | count |
|---|---|---|---|---|
| 0 | Arrival* | remove, add to end | Beloved, Coraline*, Dune, Emma, Arrival* | 1 |
| 1 | Coraline* | remove, add to end | Beloved, Dune, Emma, Arrival*, Coraline* | 2 |
| 2 | Emma | nothing | unchanged | 2 |
| 3 | Arrival* | remove, add to end | Beloved, Dune, Emma, Coraline*, Arrival* | 3 |
| 4 | Arrival* | remove, add to end | unchanged | 4 |
Two separate faults show up. First, two books were never examined at all. Removing Arrival at index 0 slid Beloved into index 0 just as i moved on to 1, and removing Coraline at index 1 slid Dune into index 1 just as i moved on to 2. That is the ArrayList bug from Lesson 11, and it did no harm here only because Beloved and Dune happened to be unfinished. The second list shows the harm: removing Holes slid Ivanhoe into index 0, i moved on to 1, and Ivanhoe, which is finished, was never moved. The other fault is rows 3 and 4. A remove followed by an add leaves the size unchanged, so the loop runs on into the books it has just moved to the end and moves them again, once as Arrival reaches index 3 and again as it reaches index 4, which is why the count was 4 and why Coraline and Arrival finished in the wrong order.
Against a rubric in the College Board's style, this answer loses the point for accessing every book exactly once, and the algorithm point, since the list is wrong and so is the count. It keeps the point for calling isFinished on a book from the list, which it does correctly.
The answer that runs backwards
Lesson 11 gave a cure for the skipping bug: traverse from the end, so that a removal only shifts books you have already examined. It works for removal. Here is what it does to this question:
for (int i = books.size() - 1; i >= 0; i--) {
if (books.get(i).isFinished()) {
books.add(books.remove(i));
count++;
}
}
before: [Arrival*, Beloved, Coraline*, Dune, Emma]
after: [Beloved, Dune, Emma, Coraline*, Arrival*], returned 2
before: [Holes*, Ivanhoe*, Jaws]
after: [Jaws, Ivanhoe*, Holes*], returned 2
The count is right now, and nothing is skipped or moved twice, because the moved books go behind the index, where a backwards loop never looks again. But the finished books come out in reverse order: Coraline was found first, since the loop starts at the end, so it went to the end first, and Arrival went behind it. The question said the finished books must keep their original order, and the example says Arrival, Coraline. A fix that is right for one question is wrong for a slightly different one, and the only defence is to check the answer against every sentence of the specification, then against the example. This answer loses the algorithm point and keeps the rest.
A model answer, and a second one
The simplest correct answer separates the two jobs. First take the finished books out, in order, into a list of their own, advancing the index only when nothing was removed. Then add them back at the end.
public int moveFinishedToEnd() {
ArrayList<Book> finished = new ArrayList<Book>();
int i = 0;
while (i < books.size()) {
if (books.get(i).isFinished()) {
finished.add(books.remove(i));
} else {
i++;
}
}
for (Book b : finished) {
books.add(b);
}
return finished.size();
}
before: [Arrival*, Beloved, Coraline*, Dune, Emma]
after: [Beloved, Dune, Emma, Arrival*, Coraline*], returned 2
before: [Holes*, Ivanhoe*, Jaws]
after: [Jaws, Holes*, Ivanhoe*], returned 2
Both lists come out as the specification requires. The while loop never skips a book, because when a book is removed the next one slides into position i and is examined on the very next check. It never revisits a moved book, because the moved books are held elsewhere until the loop has finished. And the enhanced for loop that adds them back is safe, because it traverses finished while changing books, a different list.
A second correct answer keeps everything in one list by counting passes instead of positions. Each pass handles exactly one of the original books, either moving it to the end or stepping past it, so the loop runs exactly as many times as there were books at the start:
int count = 0;
int i = 0;
int n = books.size();
for (int k = 0; k < n; k++) {
if (books.get(i).isFinished()) {
books.add(books.remove(i));
count++;
} else {
i++;
}
}
return count;
It printed exactly the same four lines as the first model answer. Either would earn full marks on a rubric like this one:
| Point | Criterion |
|---|---|
| 1 | Accesses every book in the original list exactly once (no bounds errors, none skipped or revisited) |
| 2 | Calls isFinished on a book from the list |
| 3 | Removes finished books from their positions, or builds the rearranged list from separate lists |
| 4 | Leaves all unfinished books first and all finished books last, each group in its original order (algorithm) |
| 5 | Returns the number of finished books |
The upshot: when a loop changes the list it is walking, write down what happens to the index and the size on each kind of step. Almost every ArrayList error on the exam is one of three: skipping the element that slid into place, revisiting elements added at the end, or reversing an order that the question wanted kept.
Question 4: HeightMap
This question involves a map of land heights. You will write one method of the HeightMap class.
public class HeightMap {
/** The heights of the land, in metres, one per location.
* heights has at least one row and one column, and all rows have the same length. */
private int[][] heights;
/** Returns the number of lookouts on the map, as described below. */
public int countLookouts() {
/* to be implemented */
}
// There may be instance variables, constructors, and methods that are not shown.
}
A location is a lookout if its height is strictly greater than every other height in its row and strictly greater than every other height in its column. Write the method countLookouts, which returns the number of lookouts. For example, the map below has two lookouts, the 40 in row 0 and the 44 in row 2. The 36 in row 1 is the highest in its row but not in its column, where the 39 above it is higher.
| column 0 | column 1 | column 2 | column 3 | |
|---|---|---|---|---|
| row 0 | 12 | 40 | 39 | 9 |
| row 1 | 31 | 22 | 36 | 30 |
| row 2 | 18 | 25 | 20 | 44 |
The test program runs each answer on three maps: this 3 by 4 map, called valley; a 2 by 3 map called plateau whose rows are 5, 7, 7 and 6, 2, 3; and a map of a single location holding 8.
The answer that found no lookouts at all
The natural first answer checks each location against its whole row and its whole column:
private boolean highestInRow(int r, int c) {
for (int k = 0; k < heights[r].length; k++) {
if (heights[r][k] >= heights[r][c]) {
return false;
}
}
return true;
}
with highestInColumn written the same way down column c. It reported:
valley: 0
plateau: 0
single: 0
Trace the 40 at row 0, column 1. The loop compares it with 12, then with the height at column 1, which is the 40 itself. 40 >= 40 is true, so the method returns false, and it does that for every location on every map: each height is always at least as big as itself. The definition said greater than every other height, and the word other has to become code. There are two ways to write it. Skip the location's own position, with k != c in the row check and k != r in the column check, and keep >= so that a tie with another location disqualifies it. Or compare with >, which is never true of a height and itself, but then a tie would no longer disqualify anything, which breaks the plateau. The first way matches the definition exactly.
The answer that only works on square maps
for (int r = 0; r < heights[0].length; r++) {
for (int c = 0; c < heights.length; c++) {
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at HeightMap.highestInRow(HeightMap.java:21)
at HeightMap.countLookouts(HeightMap.java:12)
at Main.main(Main.java:15)
The bounds are swapped: heights.length is the number of rows and heights[0].length is the number of columns, as Lesson 12 established. On the 3 by 4 valley, r ran up to 3, and heights[3] does not exist, which is the index 3 in an array of length 3 that the message reports. On a square map this answer would run perfectly, which is exactly how it survives a test on a square example. It loses the point for accessing every element without bounds errors, whatever its other parts do.
The model answer
public int countLookouts() {
int count = 0;
for (int r = 0; r < heights.length; r++) {
for (int c = 0; c < heights[0].length; c++) {
if (highestInRow(r, c) && highestInColumn(r, c)) {
count++;
}
}
}
return count;
}
private boolean highestInRow(int r, int c) {
for (int k = 0; k < heights[r].length; k++) {
if (k != c && heights[r][k] >= heights[r][c]) {
return false;
}
}
return true;
}
private boolean highestInColumn(int r, int c) {
for (int k = 0; k < heights.length; k++) {
if (k != r && heights[k][c] >= heights[r][c]) {
return false;
}
}
return true;
}
valley: 2
plateau: 1
single: 1
The valley gives the two lookouts the question named. The plateau checks ties: the two 7s in row 0 are each disqualified by the other, since 7 >= 7, and the 6 in row 1 beats the 2 and the 3 beside it and the 5 above it, so it is the one lookout. The single location is a lookout because there is no other height to beat, which is what "greater than every other height" means when there are none: both helper loops find nothing to object to and return true. The two helpers each return false the moment they find a counterexample, the same early-exit shape as a linear search, and the count only rises when both return true. A six-point rubric in the College Board's style might read:
| Point | Criterion |
|---|---|
| 1 | Accesses every element of heights (no bounds errors) |
| 2 | Compares a location's height with the other heights in its row |
| 3 | Compares a location's height with the other heights in its column |
| 4 | Excludes the location itself from its own comparisons, with ties to another location disqualifying it |
| 5 | Identifies a lookout only when both the row and the column conditions hold (algorithm) |
| 6 | Counts the lookouts and returns the count (algorithm) |
The helpers are not required. An answer with all four loops written out inside countLookouts earns the same points; the helpers simply make each condition short enough to check by eye, which matters when nothing will compile it for you.
Practising on the real thing
Once you can write answers like these, work on the College Board's own released questions. AP Central posts the free-response questions from the three most recent exams, with their scoring guidelines, sample student responses with commentary, and scoring statistics. The page itself warns that questions written before the 2025-26 revision do not align completely with the current exam, so an older question may ask for something, such as a subclass, that the current exam does not. Do them under exam conditions: a timer, no compiler, the Quick Reference open. Then score your answer against the published guidelines before you type it into a compiler. The comparison between what you thought you wrote and what the scoring guidelines reward is where the learning is.
Common misconceptions
- "Looping backwards fixes every ArrayList removal problem." It fixes skipping. On this question it reversed the order of the finished books, which the specification forbade.
- "Remove then add at the end is harmless inside a for loop." The size stays the same, so the loop reaches the moved elements again; the first answer moved Arrival three times.
- "Greater than every height in its row includes itself." Nothing is greater than itself; the definition says every other height, and the code must skip the location's own position.
- "heights.length and heights[0].length are interchangeable if I am careful." They are only interchangeable on a square map, which is precisely the kind of example that hides the mistake.
- "A location with nothing to compare with cannot be a lookout." The 1 by 1 map's only location beats every other height, because there are none, and the model answer counts it.
Putting it together
Question 3 asks you to use and change an ArrayList, and the danger is a loop whose index and size change under it: removing an element slides the next one into the current index, adding to the end brings moved elements back into the loop's path, and a backwards loop reverses the order of whatever it moves. Separate the removing from the adding, or count passes rather than positions, and trace both of the question's examples. Question 4 asks you to analyse a 2D array, and the dangers are swapped bounds, which only square examples hide, and conditions such as "every other" that have to be turned into code exactly. Write small helpers for each condition, test them on the example, on a tie and on the smallest possible grid, and count only when every condition holds.
The core of it: a plausible wrong answer is usually right on the easy example. Find the example where it breaks, and you have found the point it would lose.
That ends the course. You have written, compiled and run every kind of program the exam asks about, and traced each of the algorithms it names; the rest is practice under time, with the released questions and a timer.
Sources
- College Board. (2025). AP Computer Science A Course and Exam Description, Exam Information (Question 3: Data Analysis with ArrayList, 5 points, one method that uses, analyses and manipulates data in an ArrayList; Question 4: 2D Array, 6 points, one method on a 2D array) and Scoring Guidelines (the sample rubrics' criteria for accessing all elements with no bounds errors and their algorithm points). apcentral.collegeboard.org
- Oracle. (2023). Java Platform, Standard Edition 21 API Specification: class ArrayList (remove shifts any subsequent elements to the left, subtracting one from their indices; add appends the element to the end of the list). docs.oracle.com
- Oracle. (n.d.). The Java Tutorials: Arrays (a multidimensional array is an array whose components are themselves arrays, so each row is its own array with its own length). docs.oracle.com
- College Board. (n.d.). AP Computer Science A Past Exam Questions. AP Central (free-response questions from the three most recent exams, with scoring guidelines, sample responses and scoring statistics; questions from before the 2025-26 revision do not completely align with the current exam). Accessed September 2026. apcentral.collegeboard.org
- Key terms
- Traversal with removal
- A loop that removes elements from the list it walks, where the index must not advance past the element that slides into place.
- Relative order
- The order of a group of elements among themselves, which a rearrangement may be required to keep.
- Pass counting
- Looping once per original element, rather than per position, so moved elements are not visited twice.
- Lookout
- In the HeightMap question, a location strictly higher than every other location in its row and in its column.
- Swapped bounds
- Using the column count as the row limit, or the reverse; hidden by square examples, fatal on any other shape.
- Early exit
- Returning false from a loop the moment a counterexample is found, as a linear search returns on a match.
- Released questions
- The College Board's published free-response questions from recent exams, with scoring guidelines and samples.