LLD Dojo

Java Bridge · J4

Construction — final fields, no initialiser lists, and static factories

The idea

Construction

Take a Tariff holding the three rates from ParkingLotApi. In C++ that is one constructor, an initialiser list, and default arguments for the rates nobody changes. Java has none of them.

There are no initialiser lists. Every final field is assigned in the constructor body, and javac proves the assignment happens exactly once on every path. Return early from a branch and you get variable truckMinor might not have been initialized. Assign twice and you get variable carMinor might already have been assigned.

Both messages are one guarantee: a Tariff that exists has its rates. The guarantee sits on the field, so it travels with the object. A const Tariff& constrains that reference only, and another holder may have a non-const one. final fields plus a private constructor is what makes a Java object shareable without a copy.

There are no default arguments either. The replacement is a second constructor delegating with this(...), and it runs out the moment two defaults have the same type: two (int, int) constructors will not compile. LotConfig.withGraceMinutes(10, 15) and LotConfig.withFreeHours(10, 1) both do, because a static factory has a name. It also rejects bad input before an object exists, and can return a cached instance.

Then the ordering, which C++ has no analogue for. super() runs first, then field initialisers top to bottom, then the constructor body. A base constructor calling an overridable method reaches the subclass override while every subclass field is still null.


Coming from C++

From C++ — building the same Tariff both ways

The parking lot's tariff is three rates that never change after construction. You have written this class hundreds of times. Every mechanism you would reach for is missing here, and the replacements are not one-to-one.

The immutable value

C++ — an initialiser list, and const members

class Tariff {
public:
    Tariff(long motorbikeMinor, long carMinor, long truckMinor)
        : motorbikeMinor_(motorbikeMinor),   // const members are initialised here,
          carMinor_(carMinor),               // before the body runs, and only here
          truckMinor_(truckMinor) {}

private:
    const long motorbikeMinor_;
    const long carMinor_;
    const long truckMinor_;
};

Java — the body is the only place

public final class Tariff {

    private final long motorbikeMinor;
    private final long carMinor;
    private final long truckMinor;

    private Tariff(long motorbikeMinor, long carMinor, long truckMinor) {
        this.motorbikeMinor = motorbikeMinor;
        this.carMinor = carMinor;
        this.truckMinor = truckMinor;
    }
}

There is no syntax between the parameter list and the {. A final field is assigned by an ordinary statement, and javac runs a definite-assignment analysis over every path through the constructor to prove each one is assigned exactly once.

Two errors come out of that analysis, and they are the two you will meet first.

Miss a path. Add a guard that returns early:

    private Tariff(long motorbikeMinor, long carMinor, long truckMinor) {
        this.motorbikeMinor = motorbikeMinor;
        this.carMinor = carMinor;
        if (truckMinor < 0) {
            return;
        }
        this.truckMinor = truckMinor;
    }
Tariff.java:11: error: variable truckMinor might not have been initialized
            return;
            ^
1 error

Note where the caret lands. Not on the field, and not on the assignment. It lands on the return, because that is the statement that reaches the end of the constructor with the field still unset. An empty constructor body gets the same message pointed at the closing brace.

Assign twice. Clamp a bad rate instead of returning:

        this.carMinor = carMinor;
        if (carMinor < 0) {
            this.carMinor = 0;
        }
T2.java:8: error: variable carMinor might already have been assigned
            this.carMinor = 0;
                ^
1 error

The related message, and the one the spec for this lesson names, appears when you assign a final field from anywhere that is not a constructor:

    public void raiseTo(long newRate) {
        this.carMinor = newRate;
    }
T5.java:10: error: cannot assign a value to final variable carMinor
        this.carMinor = newRate;
            ^
1 error

Same rule, three messages. might not have been initialized means a path assigned it zero times, might already have been assigned means a path could assign it twice, and cannot assign a value to final variable means the code is not in a constructor at all.

One more that C++ does not have an equivalent for. Building a list in a loop:

    public T7(int n) {
        for (int i = 1; i <= n; i++) {
            this.spotIds = List.of("S" + i);
        }
    }
T7.java:7: error: variable spotIds might be assigned in loop
            this.spotIds = List.of("S" + i);
                ^
T7.java:9: error: variable spotIds might not have been initialized
    }
    ^
2 errors

n could be 0, and it could be 5, so javac refuses both ways at once. The fix is the shape SpotGrid uses in the reference solution: accumulate into a local, assign the field once afterwards. SpotNumbering in worked/ does the same.

final is not const, and here it is the stronger one

The word looks like a translation and it is not. const in C++ mostly attaches to the access path: const Tariff&, long rate() const. final attaches to the declaration.

void audit(const Tariff& t);      // this function cannot mutate t
void audit(Tariff t)              // says nothing about t at all

Java has no way to write that parameter. final Tariff t stops you reassigning the local t and permits every mutation of the object. So a Java method receiving an object has no compiler-checked promise about it. That sounds like a loss, and it is why the convention went the other way: immutability becomes a property of the class, not of the reference.

That is the direction where final wins. A const Tariff& guarantees nothing about what other holders do, because a non-const alias can exist elsewhere and mutate under you. A Tariff whose fields are all final, of immutable types, in a final class, cannot be mutated by anyone. Every holder gets the guarantee, so:

The price is stated plainly: no partial immutability. There is no const overload, so a class is immutable or it is not. A mutable class you wanted to lend out read-only needs an interface carrying only the read methods.

No default arguments

C++ — one constructor covers four call shapes

class LotConfig {
public:
    explicit LotConfig(int standardSpots,
                       Tariff tariff = Tariff::standard(),
                       int graceMinutes = 0,
                       int freeHours = 0);
};

Java — a second constructor that delegates

    public LotConfig(int standardSpots) {
        this(standardSpots, Tariff.standard(), 0, 0);
    }

    public LotConfig(int standardSpots, Tariff tariff, int graceMinutes, int freeHours) {
        // validation, then the only four field assignments in the class
    }

this(...) is C++'s delegating constructor with the same restriction: it has to come first.

T3.java:17: error: call to this must be first statement in constructor
        this(a, b);
            ^
1 error

One canonical constructor validates and assigns; every other constructor delegates to it. That is the whole pattern, and it holds until two defaults have the same type.

Where telescoping dies, and why a factory has a name

Two new requirements arrive. A grace period in minutes, and curveball 02's free first hours. Both want a two-argument form:

    public LotConfig(int standardSpots, int graceMinutes) { … }
    public LotConfig(int standardSpots, int freeHours)    { … }
T4.java:10: error: constructor T4(long,long,long) is already defined in class T4
    public T4(long motorbikeRupees, long carRupees, long truckRupees) {
           ^
1 error

A constructor's name is the class name, so its parameter types are its entire identity. Two meanings with one shape cannot coexist. C++ has the same rule and you rarely feel it, because default arguments meant you only ever wrote one constructor.

Static factories have names:

    public static LotConfig withGraceMinutes(int standardSpots, int graceMinutes) { … }
    public static LotConfig withFreeHours(int standardSpots, int freeHours)       { … }

Both compile, both read correctly at the call site, and LotConfig.withFreeHours(10, 1) cannot be confused with the other one by a reader or by overload resolution. Tariff in worked/ carries the same pair: perStartedHourMinor(1000, 2000, 4000) and perStartedHourRupees(10, 20, 40), three longs each.

This is the reason a C++ developer has never needed a static factory. The other three reasons are real but weaker, and you have seen them all in C++ as free functions:

ReasonWhat it buysC++ equivalent you already use
It has a nameTwo factories, one parameter listfree function makeX
It can validate firstThrows before an object exists, so no half-built statefactory function returning optional
It can return a cached instanceTariff.standard() hands out one objecta static const singleton
It can return a subtypeThe caller sees the interface, never the classfactory returning unique_ptr<Base>

And the threshold, because a factory is not free

Use new when the class has one way in and the constructor cannot fail. new Vehicle("KA01AB1234", CAR) is honest; Vehicle.of(...) adds a name to read and a layer to step through for nothing.

Reach for a static factory when one of these is true:

  1. Two ways in share a parameter list. This one is forced, not a preference
  2. The class is immutable and instances are worth sharing, like Tariff.standard()
  3. Construction can fail on its arguments and you want the failure before an object exists
  4. Callers should depend on an interface, which is exactly Entry.create() below

If none of the four holds, a public constructor is the smaller thing to read. STANDARD v1.0's D3 level 3 anchor reads "the seam set is minimal", and the tag for failing it is over-engineered. A factory nobody needed costs you score rather than earning it.

Entry.create(), the one static factory you will type in every attempt

This is real, and it is corpus/parking-lot/contract/Entry.java:

public final class Entry {

    /** A lot with 10 standard spots on one floor. Return your implementation. */
    public static ParkingLotApi create() {
        return new ParkingLot(10);   // ← EDIT THIS LINE: your class, your constructor
    }

    private Entry() {}
}

Every hidden test calls Entry.create() and nothing else of yours. Reason 4 in the table is the whole design: the return type is the interface, so the grader compiles against ParkingLotApi while the body names ParkingLot and your constructor. A constructor could not do this job, because new has to name a concrete class and that name would have to be one the tests knew.

Which makes the last line worth reading too. private Entry() {} on a class of static members is the Java idiom for "not instantiable". Leave it out and Java supplies a public no-argument constructor, so new Entry() compiles and means nothing. C++ gives you a free namespace for this; Java has no free functions, so the holder is a class you have to close off by hand.

Entry.create() also shows why the convenience constructor earns its place. The reference ParkingLot has a canonical ParkingLot(SpotGrid, SpotAllocator, PricingPolicy) and a ParkingLot(int) that delegates to it with this(new SpotGrid(n), new FirstFitAllocator(), new FlatHourlyPricing()). That is what keeps this line one line.

Initialisation order, and the trap with no C++ analogue

C++ — a virtual call from a constructor uses the base's version

class SpotNumbering {
public:
    explicit SpotNumbering(int count) {
        for (int i = 1; i <= count; ++i) ids_.push_back(label(i));  // SpotNumbering::label
    }
    virtual std::string label(int n) const { return "S" + std::to_string(n); }

private:
    std::vector<std::string> ids_;
};

During a C++ base constructor the object's dynamic type is the base. The vtable pointer is updated as each stage completes, so label(i) resolves to SpotNumbering::label even when the object being built is a PrefixedNumbering. Declare label pure virtual instead and the call is undefined behaviour, which libstdc++ turns into a run-time abort reading pure virtual method called.

Java — the subclass override runs, on an object with no fields yet

    protected SpotNumbering(int count) {
        List<String> ids = new ArrayList<>(count);
        for (int i = 1; i <= count; i++) {
            ids.add(label(i));          // PrefixedNumbering.label, every time
        }
        this.spotIds = List.copyOf(ids);
    }

There is one vtable and it is complete from the first instruction. label(i) dispatches to the override immediately, and the override reads prefix, which PrefixedNumbering's constructor has not reached yet. Real output from worked/:

new PrefixedNumbering(3, "S").spotIds() -> [null1, null2, null3]

No exception. No warning from plain javac. Wrong spot ids, in production.

The same bug one method call further along does throw, because a reference field defaults to null rather than to a sensible-looking empty value:

new PaddedNumbering(3, "a") threw java.lang.NullPointerException
  message: Cannot invoke "String.toUpperCase()" because "this.prefix" is null
  top frames:
    at PaddedNumbering.label(SpotNumbering.java:67)
    at SpotNumbering.<init>(SpotNumbering.java:25)
    at PaddedNumbering.<init>(SpotNumbering.java:61)

Read the three frames bottom to top and the mechanism is stated for you. PaddedNumbering's constructor called SpotNumbering's, which called PaddedNumbering.label. Java 21's helpful message names the field, this.prefix, which is the fastest diagnosis you will get all week.

The order the trace in worked/ prints:

     1  label(1) runs inside SpotNumbering's constructor; prefix is null
     2  field initialiser, declared first
     3  instance initialiser block, declared between the two fields
     4  field initialiser, declared second
     5  constructor body: the first statement that can see prefix=S

So: super() completes, then field initialisers and instance initialiser blocks run interleaved in declaration order, then your constructor body. Field initialisers are not special. They are constructor statements that javac moves for you, which is why an initialiser referring to a field declared below it fails with illegal forward reference.

The one flag that catches this

javac has a lint category for it, off by default:

> javac -Xlint:this-escape -d out lessons/J4/worked/src/*.java
lessons\J4\worked\src\SpotNumbering.java:25: warning: [this-escape] possible 'this' escape before subclass is fully initialized
            ids.add(label(i));
                         ^
1 warning

One warning, on the exact line. Plain javac on the same sources prints nothing and exits 0. It fires on any constructor of an extensible class that calls an overridable method, so it finds this class of bug before a null does.

The delta table

C++Java
member initialiser listassignments in the constructor body, nothing before {
const memberfinal field, assigned exactly once on every path
const T& parameterno equivalent; make the class immutable instead
default argumentsa second constructor delegating with this(...)
overload on default-argument countstatic factories, because they have names
copy constructorusually nothing to write; final fields make the object shareable
explicitno equivalent; Java has no converting constructors
a virtual call in a constructor runs the base versionit runs the subclass override, on null fields
compiler warns about a pure virtual call in a constructorsilent unless you pass -Xlint:this-escape
free makeThing() in a namespacestatic factory on the class; there are no free functions
private constructor to block constructionsame, plus private Entry() {} to block instantiation

The habit that will cost you time today

Writing a validate() or init() method and calling it from the constructor. In C++ that is ordinary. In Java, if the class can be extended and the method can be overridden, you have built the trap above.

Two fixes, and they are not equivalent. Make the helper static, so it cannot read a field at all, and pass its result to the field or to super(...). Or make the class final and the helper private, so no override can exist. FixedNumbering in faded/ takes the first route, and if you try to route an instance method through super(...) the compiler stops you:

T8.java:11: error: cannot reference this before supertype constructor has been called
        super(labels(count, prefix));
              ^
1 error

Worked walkthrough

NOTES — six files, one private constructor, and one trap that ships

Compile and run from the directory holding the sources:

..\..\..\.toolchain\jdk-21\bin\javac.exe -d out *.java
..\..\..\.toolchain\jdk-21\bin\java.exe -cp out Main

Plain javac exits 0 and prints nothing. Real output, from exactly this code:

== Tariff: named factories, one private constructor ==
standard()                      Tariff[motorbike=1000, car=2000, truck=4000]
perStartedHourRupees(10, 20, 40) Tariff[motorbike=1000, car=2000, truck=4000]
standard() twice, same object?   true
rejected before any Tariff existed: a car rate cannot be negative: -50

== LotConfig: no default arguments, so this(...) and named factories ==
new LotConfig(10)               LotConfig[spots=10, grace=0m, free=0h, Tariff[motorbike=1000, car=2000, truck=4000]]
withGraceMinutes(10, 15)        LotConfig[spots=10, grace=15m, free=0h, Tariff[motorbike=1000, car=2000, truck=4000]]
withFreeHours(10, 1)            LotConfig[spots=10, grace=0m, free=1h, Tariff[motorbike=1000, car=2000, truck=4000]]

== The virtual call from a constructor ==
new PrefixedNumbering(3, "S").spotIds() -> [null1, null2, null3]
new PaddedNumbering(3, "a") threw java.lang.NullPointerException
  message: Cannot invoke "String.toUpperCase()" because "this.prefix" is null
  top frames:
    at PaddedNumbering.label(SpotNumbering.java:67)
    at SpotNumbering.<init>(SpotNumbering.java:25)
    at PaddedNumbering.<init>(SpotNumbering.java:61)

== Initialisation order, printed in the order it happens ==
  new TracedNumbering(1, "S"):
     1  label(1) runs inside SpotNumbering's constructor; prefix is null
     2  field initialiser, declared first
     3  instance initialiser block, declared between the two fields
     4  field initialiser, declared second
     5  constructor body: the first statement that can see prefix=S
  ids: [S1], fields: 2  field initialiser, declared first then 4  field initialiser, declared second

== SafeNumbering: nothing overridable runs during construction ==
new SafeNumbering(3, "S").spotIds() -> [S1, S2, S3]

Three lines in there are the lesson. standard() twice, same object? true, [null1, null2, null3], and the order of the numbered trace.


Tariff.java

    private static final Tariff STANDARD = new Tariff(1000L, 2000L, 4000L);

One object for the tariff the whole corpus uses. This is only safe because every field below is final and every field's type is a primitive. Nobody holding STANDARD can change what another holder sees. Make one field non-final and this line becomes a shared mutable global, which is the bug synchronized gets added to paper over. The static initialiser runs once, when Tariff is first touched, before any of your code that mentions the class.

    private final long motorbikeMinor;
    private final long carMinor;
    private final long truckMinor;

final here is what lets standard() skip the copy. Drop it and the class still compiles, still passes every test in this lesson, and stops being shareable. Nothing in Java warns you. The invariant is: after any constructor returns, these three values are fixed for the object's whole life.

    private Tariff(long motorbikeMinor, long carMinor, long truckMinor) {
        this.motorbikeMinor = motorbikeMinor;
        this.carMinor = carMinor;
        this.truckMinor = truckMinor;
    }

private, and it is the only writer of any field in the class. Two things follow. No caller can build a Tariff that skipped validation, and there is exactly one place to look when a rate is wrong. javac enforces the second half: assigning any of these three from a method gives error: cannot assign a value to final variable carMinor.

this. is not decoration here. The parameters share the field names, so without this. the line motorbikeMinor = motorbikeMinor; assigns the parameter to itself. That compiles, and the field is then reported unassigned, which is the one time the error message saves you.

    public static Tariff standard() {
        return STANDARD;
    }

Returns the field, not new Tariff(1000L, 2000L, 4000L). Main prints standard() twice, same object? true because of this line. LotConfig's test asserts assertSame, not assertEquals, so a version that constructs a fresh equal object fails. A constructor cannot do this. new always allocates.

    public static Tariff perStartedHourMinor(long motorbike, long car, long truck) {
        requireChargeable(motorbike, "motorbike");
        requireChargeable(car, "car");
        requireChargeable(truck, "truck");
        return new Tariff(motorbike, car, truck);
    }

Every check runs before new. That is the difference from a validating constructor. The gap is smaller than it sounds, and it is real. A constructor that throws has already run super() and every field initialiser, so a half-built object existed. It was reachable from anything those steps handed this to. Here, nothing was allocated. Main shows the message: rejected before any Tariff existed: a car rate cannot be negative: -50.

The message names which rate. requireChargeable(car, "car") passes the label because IllegalArgumentException: -50 would tell you nothing at 1am. GapTest asserts the message contains "car" for exactly that reason.

    public static Tariff perStartedHourRupees(long motorbike, long car, long truck) {
        return perStartedHourMinor(motorbike * 100L, car * 100L, truck * 100L);
    }

Three longs, the same three types as the factory above it. Two constructors with these signatures do not compile: error: constructor Tariff(long,long,long) is already defined in class Tariff. Two static factories do, because a method's name is part of its identity and a constructor's name is the class. This is the reason to know static factories exist, and the one with no C++ counterpart, since default arguments meant you only wrote one constructor.

It delegates rather than calling new. So the validation is inherited rather than copied, and a fourth vehicle type changes one method. GapTest checks that perStartedHourRupees(10, -20, 40) throws, which only passes if this line goes through the other factory.

    public long minorPerStartedHour(VehicleType type) {
        return switch (type) {
            case MOTORBIKE -> motorbikeMinor;

An exhaustive switch over the enum with no default. A fourth VehicleType stops compiling here, which is where a new vehicle should force a decision about its rate. Adding default -> 0 would make a new type silently free.


LotConfig.java

    public LotConfig(int standardSpots) {
        this(standardSpots, Tariff.standard(), 0, 0);
    }

The whole body is one delegation, and it assigns nothing. That is deliberate: the four fields are final, and a second assignment anywhere would not compile. So this constructor cannot drift from the canonical one, cannot skip its validation, and cannot half-apply it. GapTest proves the last part by checking new LotConfig(0) throws with at least one spot, a message that only exists in the canonical constructor.

This is also what keeps contract/Entry.java to one line. C++ would have used a default argument and written no second constructor at all.

    public LotConfig(int standardSpots, Tariff tariff, int graceMinutes, int freeHours) {
        if (standardSpots < 1) {
            throw new IllegalArgumentException("a lot needs at least one spot, got " + standardSpots);
        }

Validation before the assignments, not after. After the assignments the object is built and throwing leaves a fully-formed invalid object reachable by anything the constructor already passed this to. Here nothing has been assigned and nothing has escaped.

The message includes the value. got 0 is the difference between a one-second diagnosis and reading the call site.

        this.tariff = Objects.requireNonNull(tariff, "tariff");

requireNonNull returns its argument, so the check and the assignment are one statement. The invariant: a LotConfig never holds a null tariff, so no code downstream needs a null check. Without it the failure surfaces later, in whichever method first calls tariff.minorPerStartedHour(...), with a stack trace pointing at the wrong class. The string "tariff" is the message, and it is what tells you which of four arguments was null.

    public static LotConfig withGraceMinutes(int standardSpots, int graceMinutes) { … }
    public static LotConfig withFreeHours(int standardSpots, int freeHours)       { … }

Two (int, int) signatures, both legal, because factories have names. As constructors they would collide. And the call site reads: LotConfig.withFreeHours(10, 1) says which 1 that is, where new LotConfig(10, 1) would not. Curveball 02 is the requirement behind the second one, so this is not a hypothetical shape.

Note what these two do not do: they add no field, no interface, and no layer. Each is one return. That is the level of ceremony a factory should cost. Vehicle in contract/ has a public record constructor and no factory, and that is correct, because there is one way to build one and it cannot fail.


SpotNumbering.java — the file to read twice

The contract says spot ids are yours to invent: "S1", "1", "A-01". A base class that owns the numbering and asks the subclass what each label is called is a reasonable-looking split.

    protected SpotNumbering(int count) {
        List<String> ids = new ArrayList<>(count);
        for (int i = 1; i <= count; i++) {
            ids.add(label(i));
        }
        this.spotIds = List.copyOf(ids);
    }

The loop fills a local and the field is assigned once, after it. Not a style choice. Assigning the field inside the loop gives two errors at once: variable spotIds might be assigned in loop and variable spotIds might not have been initialized. javac cannot rule out zero iterations or two, so it refuses both.

List.copyOf is what makes the field's final mean something. final stops the reference being repointed; it says nothing about the list. Hand out the ArrayList and any caller can renumber the lot. This is the final-is-not-const trap in its most common form, and J6 returns to it.

ids.add(label(i)) is the bug. label is overridable and this is a PrefixedNumbering, so the call lands on the override, which reads prefix. PrefixedNumbering's constructor has not reached this.prefix = prefix yet, because super(count) has to complete first. The field holds null.

The consequence, unedited:

new PrefixedNumbering(3, "S").spotIds() -> [null1, null2, null3]

String concatenation with null produces "null", so the ids look almost plausible and no exception is thrown. PaddedNumbering calls prefix.toUpperCase() instead and throws, which is the luckier outcome. Java 21's message names the field:

Cannot invoke "String.toUpperCase()" because "this.prefix" is null

Neither version produces a warning from plain javac. One flag changes that:

> javac -Xlint:this-escape -d out lessons/J4/worked/src/*.java
lessons\J4\worked\src\SpotNumbering.java:25: warning: [this-escape] possible 'this' escape before subclass is fully initialized
            ids.add(label(i));
                         ^
1 warning

Exactly one warning, on line 25, which is ids.add(label(i)). Worth adding to any build where classes are extensible.

TracedNumbering, and why the order is what it is

    private final String prefix;
    private final String declaredFirst = say("2  field initialiser, declared first");
    { say("3  instance initialiser block, declared between the two fields"); }
    private final String declaredSecond = say("4  field initialiser, declared second");

The instance initialiser block sits between the two fields on purpose. The trace prints 2, 3, 4 in that order, which is the point: field initialisers and instance blocks are one sequence in declaration order, not two phases. Moving the block above declaredFirst changes the output.

All of it runs after super(count) and before the constructor body. So the numbered trace is the full rule:

  1. super(...) completes, which is where label(1) ran and read null
  2. field initialisers and instance initialiser blocks, top to bottom
  3. the constructor body

A field initialiser is a constructor statement javac relocates for you. Referring to a field declared below one gives error: illegal forward reference, which is the only visible trace of the relocation.


SafeNumbering.java — the same job, trap-free

    public SafeNumbering(int count, String prefix) {
        this.spotIds = labels(count, prefix);
    }

    private static List<String> labels(int count, String prefix) {

static is the load-bearing word. A static method has no this, so it cannot read a field that has no value yet. The compiler enforces it rather than a convention. Try to route an instance method through super(...) and you get error: cannot reference this before supertype constructor has been called.

The class is final and does not extend anything. Belt and braces: with no subclass there is no override, so nothing overridable can run during construction. This is the shape corpus/parking-lot/reference/src/SpotGrid.java uses for the identical problem, and it is the one to reach for in a timed round. A base class that calls back into its subclass is a design worth avoiding rather than a hazard worth managing.

List.copyOf inside the static helper, so the field is unmodifiable from the moment it is assigned. spotIds() then hands the field straight out with no copy. One copyOf at construction replaces one per call.


Worked source

The 6 files of the worked design

Every file below is the one the app opens, verbatim. This is the part worth reading slowly: the prose above argues for a shape, and these are the lines that have it.

worked/src/LotConfig.java74 lines

// LotConfig.java
//
// What Entry.create() has to build. C++ would give this class one constructor with three
// default arguments. Java has no default arguments, so the same shape costs a delegating
// constructor plus two named factories.
import java.util.Objects;

public final class LotConfig {

    private final int standardSpots;
    private final Tariff tariff;
    private final int graceMinutes;
    private final int freeHours;

    /**
     * The convenience form, so contract/Entry.java stays one line: new LotConfig(10).
     * It assigns nothing. It picks defaults and hands them to the canonical constructor.
     */
    public LotConfig(int standardSpots) {
        this(standardSpots, Tariff.standard(), 0, 0);
    }

    /**
     * The canonical constructor: the only one that assigns a field, and the only one that
     * validates. Every other way in ends up here, so a LotConfig that exists is a valid one.
     */
    public LotConfig(int standardSpots, Tariff tariff, int graceMinutes, int freeHours) {
        if (standardSpots < 1) {
            throw new IllegalArgumentException("a lot needs at least one spot, got " + standardSpots);
        }
        if (graceMinutes < 0) {
            throw new IllegalArgumentException("grace minutes cannot be negative: " + graceMinutes);
        }
        if (freeHours < 0) {
            throw new IllegalArgumentException("free hours cannot be negative: " + freeHours);
        }
        this.standardSpots = standardSpots;
        this.tariff = Objects.requireNonNull(tariff, "tariff");
        this.graceMinutes = graceMinutes;
        this.freeHours = freeHours;
    }

    /** Drivers who leave within this many minutes are not billed at all. */
    public static LotConfig withGraceMinutes(int standardSpots, int graceMinutes) {
        return new LotConfig(standardSpots, Tariff.standard(), graceMinutes, 0);
    }

    /** Curveball 02's rule: the first N hours are free, then the tariff applies. */
    public static LotConfig withFreeHours(int standardSpots, int freeHours) {
        return new LotConfig(standardSpots, Tariff.standard(), 0, freeHours);
    }

    public int standardSpots() {
        return standardSpots;
    }

    public Tariff tariff() {
        return tariff;
    }

    public int graceMinutes() {
        return graceMinutes;
    }

    public int freeHours() {
        return freeHours;
    }

    @Override
    public String toString() {
        return "LotConfig[spots=" + standardSpots + ", grace=" + graceMinutes + "m, free="
                + freeHours + "h, " + tariff + "]";
    }
}

worked/src/SafeNumbering.java39 lines

// SafeNumbering.java
//
// The same job as SpotNumbering, built so the trap cannot exist. This is the shape
// corpus/parking-lot/reference/src/SpotGrid.java uses for the identical problem.
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public final class SafeNumbering {

    private final List<String> spotIds;

    public SafeNumbering(int count, String prefix) {
        this.spotIds = labels(count, prefix);
    }

    /**
     * static, so it cannot read a field. That is not a style preference: an instance method
     * called from an argument to super() or from a field initialiser fails to compile with
     * "cannot reference this before supertype constructor has been called", and one called
     * from the constructor body reads fields that are not assigned yet.
     */
    private static List<String> labels(int count, String prefix) {
        Objects.requireNonNull(prefix, "prefix");
        if (count < 1) {
            throw new IllegalArgumentException("a lot needs at least one spot, got " + count);
        }
        List<String> ids = new ArrayList<>(count);
        for (int i = 1; i <= count; i++) {
            ids.add(prefix + i);
        }
        return List.copyOf(ids);
    }

    /** Already unmodifiable, so there is no defensive copy to make on the way out. */
    public List<String> spotIds() {
        return spotIds;
    }
}

worked/src/SpotNumbering.java100 lines

// SpotNumbering.java
//
// Spot ids are the candidate's to invent — "S1", "1", "A-01" — so a base class that builds
// them and asks a subclass what each one is called looks like a reasonable split. Plain javac
// accepts it silently, and it is wrong. Run Main to see how, and see NOTES.md for the one
// javac flag that does catch it.
//
// One public class per file (J1), so the three subclasses below are package-private and share
// this file with the base they extend.
import java.util.ArrayList;
import java.util.List;

public abstract class SpotNumbering {

    private final List<String> spotIds;

    /**
     * Builds every spot id up front by asking the subclass for each label.
     *
     * The loop writes into a local and the field is assigned once, after it. Assigning the
     * field inside the loop is a compile error: "variable spotIds might be assigned in loop".
     */
    protected SpotNumbering(int count) {
        List<String> ids = new ArrayList<>(count);
        for (int i = 1; i <= count; i++) {
            ids.add(label(i));
        }
        this.spotIds = List.copyOf(ids);
    }

    /** Called by the constructor above, on an object whose subclass fields are all null. */
    protected abstract String label(int n);

    /** Immutable, so handing it out cannot corrupt the numbering. */
    public List<String> spotIds() {
        return spotIds;
    }
}

/** Prints [null1, null2, null3]. No exception, no warning, wrong ids in production. */
class PrefixedNumbering extends SpotNumbering {

    private final String prefix;

    PrefixedNumbering(int count, String prefix) {
        super(count);
        this.prefix = prefix;
    }

    @Override
    protected String label(int n) {
        return prefix + n;
    }
}

/** The same bug, one method call further along, so it throws instead of lying. */
class PaddedNumbering extends SpotNumbering {

    private final String prefix;

    PaddedNumbering(int count, String prefix) {
        super(count);
        this.prefix = prefix;
    }

    @Override
    protected String label(int n) {
        return prefix.toUpperCase() + String.format("%02d", n);
    }
}

/** Prints the initialisation sequence in the order it actually happens. */
class TracedNumbering extends SpotNumbering {

    private final String prefix;
    private final String declaredFirst = say("2  field initialiser, declared first");
    { say("3  instance initialiser block, declared between the two fields"); }
    private final String declaredSecond = say("4  field initialiser, declared second");

    TracedNumbering(int count, String prefix) {
        super(count);
        this.prefix = prefix;
        say("5  constructor body: the first statement that can see prefix=" + prefix);
    }

    @Override
    protected String label(int n) {
        say("1  label(" + n + ") runs inside SpotNumbering's constructor; prefix is " + prefix);
        return "S" + n;
    }

    String order() {
        return declaredFirst + " then " + declaredSecond;
    }

    private static String say(String line) {
        System.out.println("     " + line);
        return line;
    }
}

worked/src/Tariff.java74 lines

// Tariff.java
//
// The rate table from ParkingLotApi's javadoc: minor units per started hour, by vehicle type.
// Three final fields, one private constructor, and every way in is a named static method.
public final class Tariff {

    /**
     * The tariff the contract states: MOTORBIKE 1000, CAR 2000, TRUCK 4000 per started hour.
     * One instance, shared. It cannot be mutated by whoever holds it, so sharing is safe.
     */
    private static final Tariff STANDARD = new Tariff(1000L, 2000L, 4000L);

    private final long motorbikeMinor;
    private final long carMinor;
    private final long truckMinor;

    /**
     * The only place a field is assigned. Private, so nothing constructs a Tariff without
     * going through a factory that has already checked the numbers.
     */
    private Tariff(long motorbikeMinor, long carMinor, long truckMinor) {
        this.motorbikeMinor = motorbikeMinor;
        this.carMinor = carMinor;
        this.truckMinor = truckMinor;
    }

    /** The standard tariff. Callers share one object; there is nothing to copy. */
    public static Tariff standard() {
        return STANDARD;
    }

    /** Rates given in minor units (paise), which is how the contract states them. */
    public static Tariff perStartedHourMinor(long motorbike, long car, long truck) {
        requireChargeable(motorbike, "motorbike");
        requireChargeable(car, "car");
        requireChargeable(truck, "truck");
        return new Tariff(motorbike, car, truck);
    }

    /**
     * Rates given in whole rupees, which is how an operator quotes them.
     *
     * Same three parameter types as perStartedHourMinor. Two constructors could not both
     * exist; two named factories can.
     */
    public static Tariff perStartedHourRupees(long motorbike, long car, long truck) {
        return perStartedHourMinor(motorbike * 100L, car * 100L, truck * 100L);
    }

    public long minorPerStartedHour(VehicleType type) {
        return switch (type) {
            case MOTORBIKE -> motorbikeMinor;
            case CAR -> carMinor;
            case TRUCK -> truckMinor;
        };
    }

    /**
     * Runs before any Tariff exists, which is the difference that matters. A constructor that
     * threw here would have already run its super() call and its field initialisers.
     */
    private static void requireChargeable(long minor, String what) {
        if (minor < 0) {
            throw new IllegalArgumentException(
                    "a " + what + " rate cannot be negative: " + minor);
        }
    }

    @Override
    public String toString() {
        return "Tariff[motorbike=" + motorbikeMinor + ", car=" + carMinor
                + ", truck=" + truckMinor + "]";
    }
}

worked/src/VehicleType.java5 lines

// VehicleType.java
//
// The same enum as corpus/parking-lot/contract/VehicleType.java, copied here so this lesson
// compiles on its own. In a real attempt it is given, read-only, and already on the classpath.
public enum VehicleType { MOTORBIKE, CAR, TRUCK }

worked/src/Main.java66 lines

// Main.java
public final class Main {

    public static void main(String[] args) {
        factories();
        telescoping();
        trap();
        order();
        fixedTrap();
    }

    private static void factories() {
        System.out.println("== Tariff: named factories, one private constructor ==");
        System.out.println("standard()                      " + Tariff.standard());
        System.out.println("perStartedHourRupees(10, 20, 40) " + Tariff.perStartedHourRupees(10, 20, 40));
        System.out.println("standard() twice, same object?   "
                + (Tariff.standard() == Tariff.standard()));
        try {
            Tariff.perStartedHourMinor(1000L, -50L, 4000L);
        } catch (IllegalArgumentException e) {
            System.out.println("rejected before any Tariff existed: " + e.getMessage());
        }
    }

    private static void telescoping() {
        System.out.println();
        System.out.println("== LotConfig: no default arguments, so this(...) and named factories ==");
        System.out.println("new LotConfig(10)               " + new LotConfig(10));
        System.out.println("withGraceMinutes(10, 15)        " + LotConfig.withGraceMinutes(10, 15));
        System.out.println("withFreeHours(10, 1)            " + LotConfig.withFreeHours(10, 1));
    }

    private static void trap() {
        System.out.println();
        System.out.println("== The virtual call from a constructor ==");
        System.out.println("new PrefixedNumbering(3, \"S\").spotIds() -> "
                + new PrefixedNumbering(3, "S").spotIds());
        try {
            new PaddedNumbering(3, "a");
        } catch (NullPointerException e) {
            System.out.println("new PaddedNumbering(3, \"a\") threw " + e.getClass().getName());
            System.out.println("  message: " + e.getMessage());
            System.out.println("  top frames:");
            for (int i = 0; i < 3; i++) {
                System.out.println("    at " + e.getStackTrace()[i]);
            }
        }
    }

    private static void order() {
        System.out.println();
        System.out.println("== Initialisation order, printed in the order it happens ==");
        System.out.println("  new TracedNumbering(1, \"S\"):");
        TracedNumbering traced = new TracedNumbering(1, "S");
        System.out.println("  ids: " + traced.spotIds() + ", fields: " + traced.order());
    }

    private static void fixedTrap() {
        System.out.println();
        System.out.println("== SafeNumbering: nothing overridable runs during construction ==");
        System.out.println("new SafeNumbering(3, \"S\").spotIds() -> "
                + new SafeNumbering(3, "S").spotIds());
    }

    private Main() { }
}

The faded stage is not here, on purpose

In the app, the third stage of a lesson hands you the worked design with a few lines replaced by // GAP: markers, then compiles your completion and runs a JUnit suite against it. That needs javac, and a static site has no compiler — so rather than show a control that cannot work, this page stops at the worked source.

Run the app for the drill: it is the download in the header, and it works offline once unpacked.

← J3 · References, null and garbage collection — nothing owns anything, and one new leak J5 · Interfaces vs abstract classes — default methods, and where state is allowed to live →

← all lessons