Java Module 5 – Classes, objects, records and enums
Classes, objects, fields and constructors
Write a Java class with fields, constructors and methods, create objects with new, chain constructors with this(...) and see what a variable holds.
What you will learn
- Write a class with private fields, constructors and methods, and build separate objects from it with new
- Validate constructor arguments so that no invalid object is ever created
- Predict when the compiler adds a default constructor, and fix code that relied on one
- Use this and this(...) to set a hidden field and keep every check in one constructor
- Trace what assignment, ==, a method call and null do to a variable that holds a reference
Before you start
What you’ll learn · Before you start
What you will learn
- Write a class with private fields, constructors and methods, and build separate objects from it with new
- Validate constructor arguments so that no invalid object is ever created
- Predict when the compiler adds a default constructor, and fix code that relied on one
- Use this and this(...) to set a hidden field and keep every check in one constructor
- Trace what assignment, ==, a method call and null do to a variable that holds a reference
Before you start
On this page
Two metro cards, one holding 20,000 paise and one holding nothing, must change independently: a 3,500-paise fare
paid with the first must leave the second as it was. In Java a class describes what every such card holds and
does, its field: A variable declared in a class, outside its methods and constructors. Every object has its own copy of each instance field, which starts at 0, false or null until code assigns it; a static field has one copy for the whole class., constructor: The code that new runs once to set up a new object. It has the class's name and no return type, may take parameters, and puts the object into a valid state before any other code can use it. and methods, and each
new MetroCard("DL-4021", 20_000) builds one separate object: One value built from a class with new, also called an instance: it has its own copy of every instance field of the class, lives on the heap and is reached through references. from that description. A
variable such as asha does not hold the card itself, only a reference that leads to it.
This lesson builds that class one piece at a time: constructors that refuse an impossible card, the constructor the
compiler writes for you, this and constructors that call each other, two mistakes the compiler accepts without a
word, and what really happens when one variable is assigned to another. Every output below is what Java 25 printed,
in the Eclipse Temurin 25.0.4.1 build.
A class describes the objects, and each new builds one
Here is the class. It has two fields: id, which never changes once the card exists, and balance, a whole number
of paise (hundredths of a rupee) held in a long, so every addition and subtraction is exact. Both fields are
private: only code inside MetroCard can read or change them, and the
encapsulation lesson explains why that matters.
/** A stored-value metro card. Money is kept in paise, a whole number, so every sum is exact. */
class MetroCard {
private final String id;
private long balance;
/** The main constructor: every card starts here, so these checks hold for every card. */
MetroCard(String id, long openingBalance) {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("a card needs an id");
}
if (openingBalance < 0) {
throw new IllegalArgumentException("opening balance cannot be negative: " + openingBalance);
}
this.id = id;
this.balance = openingBalance;
}
/** A new card with nothing on it: the same checks, through the main constructor. */
MetroCard(String id) {
this(id, 0);
}
/** A replacement for a lost card: a new id and the old card's balance. */
MetroCard(String newId, MetroCard lost) {
if (lost == null) {
throw new IllegalArgumentException("there is no card to replace");
}
this(newId, lost.balance);
}
void topUp(long amount) {
if (amount <= 0) {
throw new IllegalArgumentException("a top-up must be positive: " + amount);
}
balance += amount;
}
/** Takes the fare and returns true, or returns false and takes nothing when the balance is too low. */
boolean payFare(long fare) {
if (fare <= 0) {
throw new IllegalArgumentException("a fare must be positive: " + fare);
}
if (fare > balance) {
return false;
}
balance -= fare;
return true;
}
String id() {
return id;
}
long balance() {
return balance;
}
String describe() {
return id + " holds " + balance + " paise";
}
} A constructor looks like a method that has the class’s name and no return type, not even void. It runs once,
when an object is created, and its job is to put the new object into a valid state. MetroCard has three
constructors; the sections below come back to the second and the third. topUp, payFare, id, balance and
describe are ordinary methods, and each call works on the card it is called on.
The program that uses the class is a compact source file, Main.java, in the same folder:
void main() {
MetroCard asha = new MetroCard("DL-4021", 20_000);
MetroCard ravi = new MetroCard("DL-7310");
IO.println(asha.describe());
IO.println(ravi.describe());
asha.payFare(3_500);
ravi.topUp(5_000);
IO.println("after a trip and a top-up:");
IO.println(asha.describe());
IO.println(ravi.describe());
boolean paid = ravi.payFare(6_000);
IO.println("ravi pays a 6000 paise fare: " + paid + ", " + ravi.describe());
MetroCard replacement = new MetroCard("DL-9902", asha);
IO.println("replacement: " + replacement.describe());
} Output
DL-4021 holds 20000 paise DL-7310 holds 0 paise after a trip and a top-up: DL-4021 holds 16500 paise DL-7310 holds 5000 paise ravi pays a 6000 paise fare: false, DL-7310 holds 5000 paise replacement: DL-9902 holds 16500 paise
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java
Running Main.java compiled MetroCard.java as well, because Main uses the class; the lesson on
growing a compact file into classes shows how the launcher finds
it. Step through the same run: main’s variables are on the left, the objects that each new built on the right, and
each call lands on one of them:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The first new. new MetroCard("DL-4021", 20_000) builds one object with its own id and balance, and asha receives a reference to it. The object holds the fields; the variable only leads to it.
- A second, separate card. new MetroCard("DL-7310") builds a second object that shares nothing with the first. It starts with 0 paise, as the second line of the run says: DL-7310 holds 0 paise.
- asha.payFare(3_500). payFare runs on the object that asha refers to. The fare fits, so it takes 3,500 paise, and Asha's card holds 16,500. Ravi's card is not touched.
- ravi.topUp(5_000). topUp runs on Ravi's card only, which goes from 0 to 5,000 paise. The two lines printed after the trip show 16500 and 5000.
- ravi.payFare(6_000). A fare of 6,000 paise is more than the 5,000 on Ravi's card, so payFare returns false and takes nothing. The run prints false, and the card still holds 5,000 paise.
- A replacement card. new MetroCard("DL-9902", asha) reads the balance of the card asha refers to and builds a third object with the same 16,500 paise. Three new expressions made three cards, each with its own fields.
Each new made a separate object. The fare came off Asha’s card and the top-up went onto Ravi’s, and neither
touched the other. When Ravi tried to pay 6,000 paise with 5,000 on the card, payFare returned false and took
nothing, so the card still holds 5,000.
Key idea
A class is written once, and each new builds a separate object with its own fields; a method call works on the
one object it is called on.
Checkpoint
Check: One class, separate objects
A constructor runs for every object, so the rules go there
Before a constructor runs, Java has already made the object and set every field to a default value: 0 for
numbers, false for boolean and null for references (JLS 4.12.5, 12.5). A card whose id is null is not a
valid card, so the constructor is where the rules go, because it runs for every card ever made. The main constructor
refuses a missing id and a negative opening balance by throwing IllegalArgumentException, and then the caller
never receives an object at all. Here a program asks for a card with -500 paise on it:
void main() {
IO.println("asking for a card with -500 paise on it");
MetroCard broken = new MetroCard("DL-0001", -500);
IO.println("this line never runs: " + broken.describe());
} Output (exit status 1)
asking for a card with -500 paise on it
Printed as an error (standard error)
Exception in thread "main" java.lang.IllegalArgumentException: opening balance cannot be negative: -500 at MetroCard.<init>(MetroCard.java:12) at Invalid.main(Invalid.java:3)
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Invalid.java
The stack trace: What an uncaught exception prints: its class and message, then the calls that were open when it was thrown, the most recent first, each with its file and line, and any Caused by sections. More in “IDEs, compiler errors and your first debug” names MetroCard.<init>, which is how the JVM names a constructor,
and line 12 of MetroCard.java is the check on the balance. Step through what new did, and watch where the run
leaves the column:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The request. Invalid.java prints asking for a card with -500 paise on it, then line 3 runs new MetroCard("DL-0001", -500).
- Defaults first. Before any line of the constructor runs, the JVM makes the object and sets every field to its default value: null for the String id and 0 for the long balance.
- The id passes. The constructor's first check passes, because "DL-0001" is neither null nor blank.
- The balance fails. The second check finds -500 below zero and throws IllegalArgumentException with the message opening balance cannot be negative: -500. The stack trace places it at line 12 of MetroCard.java, inside MetroCard.<init>.
- Back to the caller. The exception ends the constructor before it assigns anything and travels back to main, which does not catch it. Line 4 never runs, and the program stops with exit status 1.
- What never happens. With a valid balance the constructor would assign both fields and new would return the reference. Here neither happens, so no code anywhere can hold a card with -500 paise.
The exception left the constructor before it assigned anything, so new never returned a reference and line 4
never ran. That is the guarantee a constructor gives: no code anywhere can hold a card that broke its rules.
Local variables are different from fields. They get no default value, and the compiler refuses to read one before
the code has assigned it, a rule called definite assignment: The compiler rule that a local variable must be assigned on every path through the code before it is read. Otherwise the file does not compile: variable might not have been initialized. More in “Variables and the eight primitive types”. Only fields and
array elements start out as 0, false or null.
Checkpoint
Check: Rules that every constructor enforces
Java writes a constructor only for a class that has none
A class that declares no constructor at all gets a default constructor: The constructor that the compiler declares for a class that declares none. It has no parameters and its body is just super(), which fails to compile when the superclass has no constructor without parameters. More in “Inheritance, super and constructor chaining” from the compiler. It takes no arguments, has the same access as the class, and does nothing except call the superclass’s constructor that takes no arguments (JLS 8.8.9). As soon as you write a constructor yourself, the default one is no longer added:
| What the class declares | What new Badge() finds |
|---|---|
| No constructor at all | The default constructor: holder stays null |
Only Badge(String holder) |
No constructor without arguments: a compile error |
Badge(String holder) and Badge() { this("guest"); } |
The constructor you wrote: holder is "guest" |
Badge below is the second row, and the program still asks for new Badge():
class Badge {
String holder;
Badge(String holder) {
this.holder = holder;
}
}
class NoArgs {
static void main() {
Badge badge = new Badge();
IO.println(badge.holder);
}
} Output (exit status 1)
Printed as an error (standard error)
NoArgs.java:11: error: constructor Badge in class Badge cannot be applied to given types;
Badge badge = new Badge();
^
required: String
found: no arguments
reason: actual and formal argument lists differ in length
1 error
error: compilation failed
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java NoArgs.java
The program does not compile: required: String means that the only constructor of Badge takes a String, and
new Badge() passed nothing. If callers still need an object without an argument, write that constructor yourself,
the way MetroCard(String id) gives a card with nothing on it.
Checkpoint
Check: When Java adds a default constructor
this names the object, and this(…) hands over to another constructor
Inside a constructor or an instance method, this is the object the code is working on (JLS 15.8.3). In
this.id = id, the parameter id hides the field of the same name, and this.id reaches past the parameter to the
field. Many classes name constructor parameters after their fields for exactly this reason: the line reads as
“this card’s id is the id I was given”.
this(...) written as a statement is a different thing. It runs another constructor of the same class on the same
new object (JLS 8.8.7.1). MetroCard(String id) contains only this(id, 0), so a card with nothing on it passes
through the same checks as every other card, and the rules are written once. Step through the two new
expressions of Main.java that hand over this way:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- new MetroCard("DL-7310"). Line 3 of Main.java runs new MetroCard("DL-7310"). The object already exists with its defaults, null and 0, and the one-parameter constructor starts. Its only statement is this(id, 0).
- Handing over. this(id, 0) runs the two-parameter constructor on the same object, with "DL-7310" and 0. It makes no second object: one new always makes exactly one.
- One set of checks. The main constructor checks the id and the balance, then this.id = id and this.balance = openingBalance set the fields. The parameter id hides the field id, so this.id is what reaches the field.
- Back in MetroCard(id). Control returns to MetroCard(String id), which has nothing left to do, and new hands the card to ravi. The run prints DL-7310 holds 0 paise.
- new MetroCard("DL-9902", asha). Line 17 builds a replacement for Asha's card, which holds 16,500 paise by then. A fresh object starts with its defaults again, and the replacement constructor runs.
- A check before this(...). Its first statement refuses a null card, before this(...) runs. Java 25 allows statements there as long as they leave the new object alone, and this one only reads the parameter lost.
- The same checks again. this(newId, lost.balance) reads the private balance of Asha's card, which code inside MetroCard may do, and hands over to the main constructor. Its checks pass, and the run prints replacement: DL-9902 holds 16500 paise.
Delegating never creates a second object: one new always makes exactly one. The replacement constructor also read
lost.balance, the private field of a different card. Java allows it, because private limits access to the code
of the class, not to one object of it.
Version note
Since Java 25 (JEP 513), a constructor may run statements before this(...) or super(...), as long as they do not
use the object being built. MetroCard(String newId, MetroCard lost) uses that to refuse a null card before it
reads lost.balance. Java 22 to 24 accept such a constructor only with preview features turned on, and earlier
releases reject any statement before this(...).
Checkpoint
Check: this and this(...)
Two constructor mistakes the compiler does not point out
Both classes below look fine at a glance. One fails with an error at the call, far from the real mistake, and the
other compiles cleanly, even with -Xlint:all, the javac option that switches on every optional warning.
mistakes/Ticket.java
class Ticket {
String from;
String to;
void Ticket(String from, String to) {
this.from = from;
this.to = to;
}
static void main() {
Ticket ticket = new Ticket("Rajiv Chowk", "Noida Sector 18");
IO.println(ticket.from + " to " + ticket.to);
}
} mistakes/Fare.java
class Fare {
long paise;
Fare(long paise) {
paise = paise;
}
static void main() {
Fare fare = new Fare(3_500);
IO.println("fare: " + fare.paise + " paise");
}
} from session import run
# A method that looks like a constructor, then a parameter assigned to itself: javac -Xlint:all prints nothing for it.
run(
"""
java mistakes/Ticket.java
javac -Xlint:all -d out mistakes/Fare.java
java -cp out Fare
""",
fails=("java mistakes/Ticket.java",),
) Output
# openjdk version "25.0.4.1" LTS
$ java mistakes/Ticket.java
mistakes/Ticket.java:11: error: constructor Ticket in class Ticket cannot be applied to given types;
Ticket ticket = new Ticket("Rajiv Chowk", "Noida Sector 18");
^
required: no arguments
found: String,String
reason: actual and formal argument lists differ in length
1 error
error: compilation failed
[exit status 1]
$ javac -Xlint:all -d out mistakes/Fare.java
$ java -cp out Fare
fare: 0 paise
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 mistakes_session.py
| What was written | What javac sees | What happens | The fix |
|---|---|---|---|
void Ticket(String from, String to) |
A method named Ticket, so no constructor |
new Ticket(…) fails: required: no arguments |
Delete void |
paise = paise; in Fare(long paise) |
The parameter assigned to itself | The field stays 0: fare: 0 paise |
this.paise = paise; |
void Ticket(String from, String to)has a return type, so it is a method that happens to share the class’s name, not a constructor. With no constructor declared, the compiler added the default one, which is why the error saysrequired: no arguments. Deletingvoidturns the method into the constructor that was meant.- In
Fare, the parameterpaisehides the fieldpaise, sopaise = paisecopies the parameter onto itself. The field keeps its default value and the fare is 0 paise.
A field that never changes after construction can be declared final, and then both mistakes stop compiling: a
final field must be assigned exactly once by the end of every constructor (JLS 8.3.1.2), so Fare fails with
“variable paise might not have been initialized”, and the void Ticket method fails because a method may not assign
a final field.
Naming a constructor parameter after its field, then forgetting this
Fare(long paise) { paise = paise; } compiles and leaves the field at 0. Write this.paise = paise;, and declare
the field final when it never changes, so that the compiler catches the slip next time.
Checkpoint
Check: Constructor mistakes javac accepts
Variables hold references, not objects
new returns a reference to the object it created, and a variable of a class type stores that reference. The object
itself lives on the heap: The memory from which the JVM allocates every object and array. Variables hold references to objects there; the garbage collector reclaims objects that no reference can reach, and code never frees them itself., the memory from which the JVM allocates every object (JVMS 2.5.3).
Assigning one variable to another copies the reference, so both names lead to the same object:
void main() {
MetroCard asha = new MetroCard("DL-1001", 10_000);
MetroCard sameCard = asha;
MetroCard ravi = new MetroCard("DL-1001", 10_000);
sameCard.payFare(2_500);
IO.println("asha: " + asha.balance() + ", ravi: " + ravi.balance());
IO.println("asha == sameCard: " + (asha == sameCard));
IO.println("asha == ravi: " + (asha == ravi));
refund(asha, 1_000);
IO.println("after refund(asha, 1000): " + asha.balance());
replace(asha);
IO.println("after replace(asha): " + asha.id());
}
/** Gets a copy of the reference, so it changes the caller's card. */
void refund(MetroCard card, long amount) {
card.topUp(amount);
}
/** Points its own copy of the reference at a new card; the caller's variable does not change. */
void replace(MetroCard card) {
card = new MetroCard("DL-9999");
} Output
asha: 7500, ravi: 10000 asha == sameCard: true asha == ravi: false after refund(asha, 1000): 8500 after replace(asha): DL-1001
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java References.java
Two variables can name one object
Text description of the diagram
The diagram shows the variables of References.java on the left and the objects they refer to on the right, after the fare has been paid.
- asha and sameCard both point to the same MetroCard object, with id DL-1001 and a balance of 7500 paise. The fare paid through sameCard came off this one object, so asha sees it too.
- ravi points to a second MetroCard object. It has the same id and started with the same 10000 paise, but it is a different object, so asha == ravi is false and its balance did not change.
The diagram shows the variables after the fare. Step through the whole program, the two methods included, and watch which arrow moves:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- One new, one card. new MetroCard("DL-1001", 10_000) builds one object, and asha holds a reference to it.
- sameCard = asha. The assignment copies the reference, not the card. Both names now lead to the same object, so sameCard is a second name for Asha's card.
- A second new. ravi gets a second object built from the same id and balance. It is a separate card in its own place on the heap.
- sameCard.payFare(2_500). The fare paid through sameCard comes off the one object both names lead to. The run prints asha: 7500, ravi: 10000.
- == on references. asha == sameCard is true, because both lead to the same object. asha == ravi is false: they are two objects, whatever values they hold. Comparing contents is the job of equals.
- refund(asha, 1_000). The call copies asha's reference into the parameter card, so card leads to the same object, and card.topUp(1_000) adds to it. The run prints after refund(asha, 1000): 8500.
- replace(asha). replace gets its own copy of the reference too, then card = new MetroCard("DL-9999") points that copy at a new object. Only the parameter moved.
- asha has not moved. asha still refers to the first card, so the last line prints after replace(asha): DL-1001. A method can change the object it is given, never which object the caller's variable refers to.
- The fare paid through
sameCardcame off Asha’s card, becauseashaandsameCardrefer to one object:sameCardis an alias: A second name for the same object: after int[] b = a; both variables refer to one array, so a change made through either is seen through the other, and a == b is true. More in “Arrays: creation, indexing and iteration” ofasha. - On references,
==asks whether both sides refer to the same object (JLS 15.21.3).asha == raviis false, although both cards were built with the same id and balance: they are two objects. Comparing objects by their contents is the job ofequals, which a later lesson of this module covers. - A parameter is a new variable that receives a copy of the argument (JLS 8.4.1).
refundused its copy to top up the card, and the caller sees the new balance.replacepointed its copy at another card, which changed nothing forasha. Java passes every argument pass-by-value: Passing a copy of each argument's value to the method's parameter, which is how Java passes every argument. For an object the value copied is a reference, so the method can change the object but never the caller's variable. More in “Methods, parameters, return and overloading”, and for an object the value is the reference.
Key idea
A variable of a class type holds a reference: assignment copies the reference, == compares references, and a
method can change the object it receives but never the caller’s variable.
Checkpoint
Check: What a class-type variable holds
null refers to no object, and the JVM names the one that was null
null: The reference that refers to no object. A variable of a class type may hold it and == can test for it, but calling a method or reading a field through it throws NullPointerException. is the reference that leads nowhere. Comparing a variable with null is
fine, but calling a method through it throws a NullPointerException, and the JVM’s helpful messages (JEP 358) say
which reference was null:
void main() {
MetroCard lost = null;
IO.println("lost == null: " + (lost == null));
lost.payFare(2_500);
IO.println("this line never runs");
} from session import run
# The launcher compiles without the names of local variables; javac -g keeps them, so the message can name one.
run(
"""
java card/Lost.java
javac -g -d out card/MetroCard.java card/Lost.java
java -cp out Lost
""",
fails=("java card/Lost.java", "java -cp out Lost"),
) Output
# openjdk version "25.0.4.1" LTS $ java card/Lost.java lost == null: true Exception in thread "main" java.lang.NullPointerException: Cannot invoke "MetroCard.payFare(long)" because "<local1>" is null at Lost.main(Lost.java:4) [exit status 1] $ javac -g -d out card/MetroCard.java card/Lost.java $ java -cp out Lost lost == null: true Exception in thread "main" java.lang.NullPointerException: Cannot invoke "MetroCard.payFare(long)" because "lost" is null at Lost.main(Lost.java:4) [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 null_session.py
lost == null printed true without any trouble, and the next line failed. The first message names "<local1>"
rather than lost. The source launcher compiles without the names of local variables, and javac leaves them out
too unless asked: by default it keeps only line numbers and the source file name. Compiled with javac -g, the
class file keeps the names as debug information: Tables that javac writes into a class file besides the bytecode: line numbers and the source file's name by default, and local variable names only with javac -g. Debuggers and exception messages use them. More in “IDEs, compiler errors and your first debug”, and the same failure names the
variable lost.
How Lost was compiled |
The message says |
|---|---|
java card/Lost.java, the source launcher |
because "<local1>" is null |
javac -g, then java -cp out Lost |
because "lost" is null |
Checkpoint
Check: null and NullPointerException
Practise: a metro card with rules
Make MetroCard keep these rules through every constructor and method. A card may hold at most MAX_BALANCE,
300,000 paise, and it is low below LOW_BALANCE, 5,000 paise.
MetroCard(String id, long balance)throwsIllegalArgumentExceptionfor anullor blank id, a negative balance and a balance aboveMAX_BALANCE.topUp(long amount)throwsIllegalArgumentExceptionfor an amount of 0 or less, and for one that would take the balance aboveMAX_BALANCE. A refused top-up leaves the balance as it was.payFare(long fare)throwsIllegalArgumentExceptionfor a fare of 0 or less. When the fare is more than the balance, it returnsfalseand takes nothing; otherwise it takes the fare and returnstrue.isLow()returnstruewhen the balance is belowLOW_BALANCE.
Copy MetroCard.java and CheckMetroCard.java into a folder named practice, then run
java practice/CheckMetroCard.java from the folder above it. The starter file passes 4 of the 12 checks:
practice/MetroCard.java
/** Practice: make every method keep the card's rules (see the lesson). The checks are in CheckMetroCard.java. */
class MetroCard {
/** The most a card may hold, in paise. */
static final long MAX_BALANCE = 300_000;
/** Below this balance, in paise, a card is low. */
static final long LOW_BALANCE = 5_000;
private final String id;
private long balance;
MetroCard(String id, long balance) {
// TODO: refuse a null or blank id, a negative balance and a balance above MAX_BALANCE.
this.id = id;
this.balance = balance;
}
void topUp(long amount) {
// TODO: refuse an amount of 0 or less, and one that would take the balance above MAX_BALANCE.
balance += amount;
}
boolean payFare(long fare) {
// TODO: refuse a fare of 0 or less; return false, and take nothing, when the fare is more than the balance.
balance -= fare;
return true;
}
boolean isLow() {
// TODO: true when the balance is below LOW_BALANCE.
return false;
}
long balance() {
return balance;
}
} practice/CheckMetroCard.java
// Checks practice/MetroCard.java against the rules of the lesson: java practice/CheckMetroCard.java
int passed = 0;
int total = 0;
void check(String rule, BooleanSupplier test) {
total++;
boolean ok;
String note = "";
try {
ok = test.getAsBoolean();
} catch (RuntimeException e) {
ok = false;
note = " (threw " + e + ")";
}
if (ok) {
passed++;
}
IO.println((ok ? "PASS " : "FAIL ") + rule + note);
}
/** True when the action is refused with an IllegalArgumentException. */
boolean refused(Runnable action) {
try {
action.run();
return false;
} catch (IllegalArgumentException expected) {
return true;
}
}
void main() {
check("a new card keeps its opening balance", () -> new MetroCard("DL-2001", 10_000).balance() == 10_000);
check("a blank id is refused", () -> refused(() -> new MetroCard(" ", 1_000)));
check("a negative opening balance is refused", () -> refused(() -> new MetroCard("DL-2002", -1)));
check("an opening balance above MAX_BALANCE is refused",
() -> refused(() -> new MetroCard("DL-2003", MetroCard.MAX_BALANCE + 1)));
check("a top-up adds to the balance", () -> {
var card = new MetroCard("DL-2004", 10_000);
card.topUp(5_000);
return card.balance() == 15_000;
});
check("a top-up of 0 is refused", () -> refused(() -> new MetroCard("DL-2005", 10_000).topUp(0)));
check("a top-up past MAX_BALANCE is refused and changes nothing", () -> {
var card = new MetroCard("DL-2006", MetroCard.MAX_BALANCE - 100);
return refused(() -> card.topUp(101)) && card.balance() == MetroCard.MAX_BALANCE - 100;
});
check("a fare the card can pay is taken", () -> {
var card = new MetroCard("DL-2007", 10_000);
return card.payFare(3_500) && card.balance() == 6_500;
});
check("a fare above the balance returns false and takes nothing", () -> {
var card = new MetroCard("DL-2008", 2_000);
return !card.payFare(3_500) && card.balance() == 2_000;
});
check("a fare of 0 is refused", () -> refused(() -> new MetroCard("DL-2009", 10_000).payFare(0)));
check("4,999 paise is low", () -> new MetroCard("DL-2010", 4_999).isLow());
check("5,000 paise is not low", () -> !new MetroCard("DL-2011", 5_000).isLow());
IO.println(passed + " of " + total + " checks pass");
if (passed < total) {
System.exit(1);
}
} from session import run
# The checks, run against the starter file.
run("java practice/CheckMetroCard.java", fails=("java practice/CheckMetroCard.java",)) Output
# openjdk version "25.0.4.1" LTS $ java practice/CheckMetroCard.java PASS a new card keeps its opening balance FAIL a blank id is refused FAIL a negative opening balance is refused FAIL an opening balance above MAX_BALANCE is refused PASS a top-up adds to the balance FAIL a top-up of 0 is refused FAIL a top-up past MAX_BALANCE is refused and changes nothing PASS a fare the card can pay is taken FAIL a fare above the balance returns false and takes nothing FAIL a fare of 0 is refused FAIL 4,999 paise is low PASS 5,000 paise is not low 4 of 12 checks pass [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
A first hint: in topUp, check amount > MAX_BALANCE - balance rather than balance + amount > MAX_BALANCE. Both
mean the same for small numbers, but the first one cannot overflow when someone passes a huge amount.
Then the questions: cards and objects, constructors, this, references and null, one at a time. The topic your
earlier answers on this page missed most comes up first, and every choice carries its own reason:
Practice that adapts to you
Practise this lesson
18 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What does Java do when a class declares no constructor at all?
The compiler adds a default constructor. It takes no arguments, has the same access as the class (package access
when the class has no access modifier), and does nothing except call the superclass’s no-argument constructor, so
the fields keep their default values of 0, false or null unless their declarations give them other values. As soon
as you declare any constructor yourself, the default one is no longer added: code that called new Badge() stops
compiling when Badge(String holder) is added. Declare a no-argument constructor yourself if callers still need one.
Two longer questions of this module start from this lesson: a method named like its class that broke two kinds of caller, and the constructors of a booking with a required name, a seat count and an optional coach preference.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A class Account declares public void Account(String id, long balance) and assigns both fields in it. New code calling new Account("A1", 500) does not compile, and old code calling new Account() gets an account whose id is null. Explain what the compiler saw, and how you would stop this mistake reaching the main branch.
-
Fresher to mid
Design the constructors of a Booking class: a passenger name and a seat count from 1 to 6 are required, and a coach preference is optional. How many constructors do you write, where does the validation live, and what can callers never create?
Key takeaways
- A class is written once and lists fields, constructors and methods; each
newbuilds a separate object with its own fields, and a method call works on the object it is called on. - Every field starts at 0,
falseornull, and the constructor runs for every object, so the rules go there: a constructor that throws leaves the caller with no object at all. - The compiler adds a no-argument default constructor only to a class that declares none, so the first constructor you write removes it.
this.fieldreaches a field hidden by a parameter, andthis(...)hands construction to another constructor of the same class, so the checks are written once and onenewstill makes one object.- A “constructor” with a return type is a method, and
paise = paiseassigns a parameter to itself; afinalfield turns both slips into compile errors. - A variable holds a reference: assignment copies it,
==compares it, a method receives a copy of it, and calling a method throughnullthrows aNullPointerExceptionthat names the null reference.
Terms in this lesson
- constructor
- The code that new runs once to set up a new object. It has the class's name and no return type, may take parameters, and puts the object into a valid state before any other code can use it.
- field
- A variable declared in a class, outside its methods and constructors. Every object has its own copy of each instance field, which starts at 0, false or null until code assigns it; a static field has one copy for the whole class.
- heap
- The memory from which the JVM allocates every object and array. Variables hold references to objects there; the garbage collector reclaims objects that no reference can reach, and code never frees them itself.
- null
- The reference that refers to no object. A variable of a class type may hold it and == can test for it, but calling a method or reading a field through it throws NullPointerException.
- object
- One value built from a class with new, also called an instance: it has its own copy of every instance field of the class, lives on the heap and is reached through references.
References
- The Java Language Specification, Java SE 25: Chapter 8, Classes (Oracle)
- The Java Language Specification, Java SE 25: 8.8.9 Default Constructor (Oracle)
- The Java Language Specification, Java SE 25: 8.8.7.1 Explicit Constructor Invocations (Oracle)
- The Java Language Specification, Java SE 25: 8.3.1.2 final Fields (Oracle)
- The Java Language Specification, Java SE 25: 8.4.1 Formal Parameters (Oracle)
- The Java Language Specification, Java SE 25: 4.12.5 Initial Values of Variables (Oracle)
- The Java Language Specification, Java SE 25: 12.5 Creation of New Class Instances (Oracle)
- The Java Language Specification, Java SE 25: 15.8.3 this (Oracle)
- The Java Language Specification, Java SE 25: 15.21.3 Reference Equality Operators (Oracle)
- The Java Virtual Machine Specification, Java SE 25: 2.5.3 Heap (Oracle)
- JEP 513: Flexible Constructor Bodies (OpenJDK)
- JEP 358: Helpful NullPointerExceptions (OpenJDK)
- The javac Command (Java SE 25): -g and -Xlint (Oracle)
- Creating Classes (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress