Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with Java temurin-25.0.4+101.0.LTS and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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.

card/MetroCard.java Java
/** 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:

Two cards, a trip, a top-up and a replacement Java · card/Main.java
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:

Interactive · step by step Three new expressions, three separate cards Step through Main.java. The square boxes are main's variables, the rounded ones the objects; watch which card each call changes.
Three new expressions, three separate cards, step 6 of 6. Showing asha, ravi, replacement, card DL-4021 (16,500 paise), card DL-7310 (5,000 paise), card DL-9902. In focus: card DL-9902.refers torefers torefers toashaa MetroCard variableravia MetroCard variablereplacementa MetroCard variablecard DL-402116,500 paisecard DL-73105,000 paisecard DL-990216,500 paise

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 In Main.java, Ravi's card starts with nothing on it. After asha.payFare(3_500) and ravi.topUp(5_000), how many paise does Ravi's card hold?

    Type a number.

    A hint

    Each call changes only the card it is called on.

    Show the answer: Question 1 of 2

    Answer: 5000 paise

    ravi.topUp(5_000) adds 5,000 paise to Ravi's card, which held 0. asha.payFare(3_500) runs on Asha's card, a separate object, so it takes nothing from Ravi's. The run prints DL-7310 holds 5000 paise.

  2. Question 2 of 2 How many MetroCard objects does Main.java create?

    Type a number.

    A hint

    Count the new expressions in main.

    Show the answer: Question 2 of 2

    Answer: 3 objects

    main has three new expressions, for asha, ravi and replacement, and each makes exactly one object. The this(...) calls inside the constructors make no extra 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:

A card with -500 paise, refused Java · card/Invalid.java
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:

Interactive · stage by stage A card that the constructor refuses Step through new MetroCard("DL-0001", -500) and watch where the run leaves the column.
A card that the constructor refuses, step 6 of 6. Showing Invalid.main runs new (gets an exception, no card), Fields start at defaults, Check the id ("DL-0001": passes), Check the balance (-500 is below 0: throws), Assign both fields (never runs), new returns a reference (never happens). In focus: Assign both fields, new returns a reference.throwsInvalid.main runs newgets an exception, no cardFields start at defaultsid null, balance 0Check the id"DL-0001": passesCheck the balance-500 is below 0: throwsAssign both fieldsnever runsnew returns a referencenever happens

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The request. Invalid.java prints asking for a card with -500 paise on it, then line 3 runs new MetroCard("DL-0001", -500).
  2. 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.
  3. The id passes. The constructor's first check passes, because "DL-0001" is neither null nor blank.
  4. 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>.
  5. 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.
  6. 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

  1. Question 1 of 2 new MetroCard("DL-0001", -500) starts. Before the first line of the constructor runs, what do the new object's id and balance hold?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: null and 0

    JLS 12.5 makes the object and sets every field to its default before any constructor code runs: id is null and balance is 0. The constructor then decides, with its checks, whether the object may be finished at all.

  2. Question 2 of 2 In Invalid.java, MetroCard broken = new MetroCard("DL-0001", -500); throws. What happens to line 4, IO.println("this line never runs: " + broken.describe());?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It never runs, because the exception ends main

    The check on the balance throws at line 12 of MetroCard.java. The exception leaves the constructor and then main, which does not catch it, so the assignment to broken and line 4 never happen.

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():

A constructor that is not there Java · mistakes/NoArgs.java
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

  1. Question 1 of 2 Badge declares only Badge(String holder). What happens to new Badge() elsewhere in the program?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It does not compile: required: String, found: no arguments

    The default constructor exists only while a class declares no constructor. Badge declares Badge(String holder), so new Badge() matches nothing, as the run shows. Declare Badge() yourself if callers need it.

  2. Question 2 of 2 class Badge { String holder; } declares no constructor. Which describes the constructor the compiler adds?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It takes no arguments, has the access of the class and only calls the superclass's constructor without arguments

    The default constructor takes no arguments, has the same access as the class and does nothing but call the superclass's constructor without arguments, so every field keeps its default or the value its declaration gives.

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:

Interactive · step by step Two constructors that hand over with this(...) Step through two new expressions of Main.java. The left column is the constructor that runs, the right column the new card's two fields.
Two constructors that hand over with this(...), step 7 of 7. Showing MetroCard(id), MetroCard(id, opening), MetroCard(newId, lost), new card: id ("DL-9902"), new card: balance (16500), Asha's card. In focus: MetroCard(id, opening), new card: id, new card: balance.this(id, 0)this(…)MetroCard(id)this(id, 0)MetroCard(id, opening)the checks and the fieldsMetroCard(newId, lost)this(newId, lost.balance)new card: id"DL-9902"new card: balance16500Asha's card16,500 paise"DL-9902", 16500

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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(...)

  1. Question 1 of 2 Inside MetroCard(String id, long openingBalance), what does each id in this.id = id; mean?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: this.id is the field of the new card, and the plain id is the parameter

    A parameter hides a field with the same name, so the plain id is the argument, and this.id is the field of the object being built. The line copies the one into the other.

  2. Question 2 of 2 new MetroCard("DL-7310") runs MetroCard(String id), which runs this(id, 0), which runs the main constructor. How many objects does that one new expression create?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1 objects

    this(...) runs another constructor on the same object. Two constructors run, one object is made, and new hands that one object to ravi.

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

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");
    }
}
A method named like its class, and a field that stays 0 Python · mistakes_session.py
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 says required: no arguments. Deleting void turns the method into the constructor that was meant.
  • In Fare, the parameter paise hides the field paise, so paise = paise copies 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

  1. Question 1 of 2 class Ticket declares void Ticket(String from, String to). Why does new Ticket("Rajiv Chowk", "Noida Sector 18") fail with required: no arguments?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: void makes Ticket(...) a method, so the class has only the default constructor, which takes no arguments

    A declaration with a return type is a method, even when it shares the class's name. Ticket therefore declares no constructor, gets the default one, and that one takes no arguments. Deleting void fixes it.

  2. Question 2 of 2 Fare(long paise) { paise = paise; } compiles and leaves the field at 0. Which change makes javac reject the mistake?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Declare the field final long paise

    final asks the compiler to prove that the field is assigned exactly once in every constructor. paise = paise assigns only the parameter, so the proof fails and the slip becomes a compile error.

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:

One card with two names, and a second card Java · card/References.java
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

asha and sameCard point to one MetroCard object holding 7500 paise; ravi points to a second card holding 10000 paise.ashasameCardraviMetroCard objectDL-10017500 paiseMetroCard objectDL-100110000 paise

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.

  1. 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.
  2. 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:

Interactive · step by step Two names for one card, and a parameter that moves Step through References.java. Square boxes are variables, rounded boxes objects; an arrow is a reference.
Two names for one card, and a parameter that moves, step 8 of 8. Showing asha, sameCard, ravi, card, first card (DL-1001, 8,500 paise), second card, a new card. In focus: asha, first card.refers torefers torefers tonow refers toashaa variable of mainsameCarda variable of mainravia variable of maincarda method's parameterfirst cardDL-1001, 8,500 paisesecond cardDL-1001, 10,000 paisea new cardDL-9999, 0 paise

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. One new, one card. new MetroCard("DL-1001", 10_000) builds one object, and asha holds a reference to it.
  2. 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.
  3. 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.
  4. 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.
  5. == 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.
  6. 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.
  7. 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.
  8. 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 sameCard came off Asha’s card, because asha and sameCard refer to one object: sameCard is 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” of asha.
  • On references, == asks whether both sides refer to the same object (JLS 15.21.3). asha == ravi is false, although both cards were built with the same id and balance: they are two objects. Comparing objects by their contents is the job of equals, 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). refund used its copy to top up the card, and the caller sees the new balance. replace pointed its copy at another card, which changed nothing for asha. 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

  1. Question 1 of 2 MetroCard a = new MetroCard("DL-5", 8_000); MetroCard b = a; b.payFare(3_000); How many paise does a.balance() return?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 5000 paise

    There is only one new, so there is one card. b = a copies the reference, and the fare paid through b comes off the card that a refers to as well: 8,000 − 3,000 = 5,000.

  2. Question 2 of 2 In References.java, replace(asha) runs card = new MetroCard("DL-9999"); on its parameter. What does asha.id() return afterwards?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "DL-1001"

    A method receives a copy of each argument. For an object that copy is a reference, so the method can change the object, as refund did, but pointing the copy elsewhere changes nothing for the caller. The run prints DL-1001.

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:

card/Lost.java Java
void main() {
    MetroCard lost = null;
    IO.println("lost == null: " + (lost == null));
    lost.payFare(2_500);
    IO.println("this line never runs");
}
Calling a method through null Python · null_session.py
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

  1. Question 1 of 2 java card/Lost.java stops with because "<local1>" is null. Why does the message not say lost?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The source launcher compiles without the names of local variables; javac -g keeps them

    Local variable names are debug information that javac writes only with -g. Without them, JEP 358's messages fall back to a number for the variable, <local1>.

  2. Question 2 of 2 MetroCard lost = null; Which line runs without an exception?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: IO.println(lost == null);

    null is a reference that leads to no object. You may store it, pass it and compare it with ==, but any method call or field access through it throws 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.

  1. MetroCard(String id, long balance) throws IllegalArgumentException for a null or blank id, a negative balance and a balance above MAX_BALANCE.
  2. topUp(long amount) throws IllegalArgumentException for an amount of 0 or less, and for one that would take the balance above MAX_BALANCE. A refused top-up leaves the balance as it was.
  3. payFare(long fare) throws IllegalArgumentException for a fare of 0 or less. When the fare is more than the balance, it returns false and takes nothing; otherwise it takes the fare and returns true.
  4. isLow() returns true when the balance is below LOW_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

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);
    }
}
The checks, run against the starter file Python · practice_session.py
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.

  1. Question 1 of 18 Two cards are made with new MetroCard("A", 100) and new MetroCard("B", 100). A fare of 40 paise is paid with the first. What happens to the second card?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: Nothing; it still holds 100 paise

    The class is written once, and each new makes an object with its own id and balance. The run of a program like this prints 60 for the first card and 100 for the second.

  2. Question 2 of 18 What does this print? MetroCard is the class of this lesson.

    Read the code, then choose one answer.

    MetroCard a = new MetroCard("DL-1", 9_000);
    MetroCard b = new MetroCard("DL-2", 9_000);
    a.payFare(4_000);
    b.topUp(1_000);
    IO.println(a.balance() + " " + b.balance());
    Show the answer: Question 2 of 18

    Answer: 5000 10000

    a starts at 9,000 and pays 4,000, leaving 5,000; b starts at 9,000 and gets 1,000, making 10,000. The program prints 5000 10000.

  3. Question 3 of 18 Where should the rule "a card's opening balance is never negative" be written so that it holds for every card?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: In the constructor, which runs for every card that is created

    A constructor runs for every object, so it is the one place where a rule about a new object can be checked once for all callers. A constructor that throws leaves the caller with no object at all.

  4. Question 4 of 18 What does this print? The MetroCard constructor throws IllegalArgumentException for a negative balance.

    Read the code, then choose one answer.

    MetroCard c = null;
    try {
        c = new MetroCard("X", -1);
    } catch (IllegalArgumentException e) {
        IO.println("caught");
    }
    IO.println(c);
    Show the answer: Question 4 of 18

    Answer: caught null

    new either returns a reference to a finished object or throws. Here it throws, the catch prints caught, and c still holds the null it started with. The lesson's MetroCard printed exactly these two lines on JDK 25.

  5. Question 5 of 18 Which of these does the compiler refuse?

    Choose one answer.

    Show the answer: Question 5 of 18

    Answer: Reading a local variable before any code has assigned it

    Fields and array elements start at 0, false or null. Local variables do not, so the compiler checks that every path assigns a local variable before it is read and refuses the file otherwise.

  6. Question 6 of 18 Code all over a program creates badges with new Badge(), and Badge had no constructors. A teammate adds Badge(String holder). Which addition keeps every new Badge() compiling and gives those badges the holder "guest"?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: Badge() { this("guest"); }

    Declaring any constructor removes the default one, so the old calls need a constructor without parameters again. Written as Badge() { this("guest"); }, it hands over to the new constructor, as the last row of the section's table shows; on JDK 25 new Badge().holder then prints guest.

  7. Question 7 of 18 class Badge { String holder; } has no constructor. What does new Badge().holder give?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: null

    With no constructor declared, Badge gets the default one, which only calls the superclass constructor. The field holder therefore keeps its default value, null.

  8. Question 8 of 18 MetroCard(String id) contains only this(id, 0). What does that line do?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: It runs the MetroCard(String, long) constructor on the same new object, so that constructor's checks apply

    this(...) delegates to another constructor of the same class, on the same object. Only one object exists, and the validation written in the main constructor runs for it.

  9. Question 9 of 18 Someone rewrites MetroCard(String id) as this.id = id; this.balance = 0; instead of this(id, 0). What is lost?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: The id check, so new MetroCard(" ") now makes a card with a blank id

    Delegating with this(...) keeps every rule in one constructor. A constructor that sets the fields itself must repeat the checks, and the first one that forgets lets an invalid card through.

  10. Question 10 of 18 MetroCard(String newId, MetroCard lost) reads lost.balance, the private field of another card. Why does it compile?

    Choose one answer.

    Show the answer: Question 10 of 18

    Answer: private limits access to the code of the MetroCard class, and this code is inside MetroCard

    private means "only code inside this class". It does not mean "only this object", so a constructor of MetroCard may read the balance of the card it replaces.

  11. Question 11 of 18 What does this print?

    Read the code, then choose one answer.

    class Fare {
        long paise;
        Fare(long paise) {
            paise = paise;
        }
    }
    // elsewhere:
    IO.println(new Fare(3_500).paise);
    Show the answer: Question 11 of 18

    Answer: 0

    Inside the constructor the parameter paise hides the field, so paise = paise assigns the parameter to itself. The field keeps its default value, 0. this.paise = paise sets the field.

  12. Question 12 of 18 Which declaration inside class Ticket is a constructor?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: Ticket(String from, String to) { … }

    The only thing that separates a constructor from a method named like the class is the missing return type. With any return type at all, the compiler treats the declaration as a method.

  13. Question 13 of 18 MetroCard is the class from this lesson. What does this print?

    Read the code, then choose one answer.

    MetroCard a = new MetroCard("DL-5", 8_000);
    MetroCard b = a;
    a = new MetroCard("DL-6", 1_000);
    b.payFare(3_000);
    IO.println(a.balance() + " " + b.balance());
    Show the answer: Question 13 of 18

    Answer: 1000 5000

    Assignment copies a reference. b = a gives b the reference to the first card; a = new MetroCard(...) then points a at a second card and leaves b alone. The fare paid through b comes off the first card, so the program prints 1000 5000 on JDK 25.

  14. Question 14 of 18 asha == ravi is false in References.java, although both cards were built with the id DL-1001 and 10,000 paise. What does == compare here?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: Whether both variables refer to the same object

    For references, == is true only when both sides lead to the same object, as asha == sameCard does. Two objects built from the same values are still two objects, so asha == ravi is false.

  15. Question 15 of 18 What does this print?

    Read the code, then choose one answer.

    void reset(MetroCard card) {
        card.topUp(500);
        card = new MetroCard("DL-9999");
        card.topUp(9_000);
    }
    
    void main() {
        MetroCard asha = new MetroCard("DL-1001", 10_000);
        reset(asha);
        IO.println(asha.id() + " " + asha.balance());
    }
    Show the answer: Question 15 of 18

    Answer: DL-1001 10500

    reset receives a copy of the reference. Through it, topUp(500) takes Asha's card to 10,500 paise. Then the copy is pointed at a new card, and the 9,000 goes onto that one, which nothing else refers to. The program prints DL-1001 10500 on JDK 25: a method can change the object it was given, but not the caller's variable.

  16. Question 16 of 18 What happens when a program calls payFare through a variable that holds null?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: A NullPointerException, whose message names the reference that was null

    null refers to no object, so a method call through it has nothing to run on. The JVM throws NullPointerException, and since JEP 358 the message says what was null.

  17. Question 17 of 18 Lost.java is compiled with javac -g -d out card/MetroCard.java card/Lost.java and run with java -cp out Lost. What does the exception message say was null?

    Choose one answer.

    Show the answer: Question 17 of 18

    Answer: "lost"

    The second run of the lesson's session prints because "lost" is null. Local variable names are debug information, which javac writes only with -g; without them the message says <local1>.

  18. Question 18 of 18 Without its first if, the replacement constructor would start with this(newId, lost.balance). What would new MetroCard("DL-9902", null) do then?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: Throw a NullPointerException while reading lost.balance

    The arguments of this(...) are worked out before the other constructor runs, and lost.balance through null throws NullPointerException. The lesson's constructor checks lost first, so the caller gets a message that says what was wrong.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A class is written once and lists fields, constructors and methods; each new builds a separate object with its own fields, and a method call works on the object it is called on.
  • Every field starts at 0, false or null, 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.field reaches a field hidden by a parameter, and this(...) hands construction to another constructor of the same class, so the checks are written once and one new still makes one object.
  • A “constructor” with a return type is a method, and paise = paise assigns a parameter to itself; a final field 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 through null throws a NullPointerException that 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.