Phase 1 · Objects

Two fivers are the same money

By the end of this page you'll have built a Money type that a set can always find, nobody can change, and nobody can add to dollars — and you'll know what static has meant all along.

⏱ ~70 minutes🧩 Needs: 1.1💻 Code: src/

Lend a friend a five-pound note on Monday. On Friday they pay you back with a different five-pound note. Nobody checks the serial number. A fiver is a fiver. Now lend the same friend your dog, and on Friday they hand back a different dog with the same name. You would mind. Very much. Some things matter because of which one they are. Others matter only for what they're worth. Java can't tell those two apart until you tell it.

Chapter 1.1 ended on a promise. .equals is a method that somebody chose to write. This chapter is where you write one. It sounds like a three-line job, and it is. Then those three lines quietly break a HashMap, and the real work starts.

So we're going to build one small, useful type — Money — and then attack it. Every attack is a real program you run, and every gap it finds gets closed.

Fair warning about one of them. At some point you'll put a fiver into a set and ask the set if it has it. It'll say no. Then you'll ask how many things it holds, and it'll say one. Both answers are honest.

What you'll be able to do

Section 1A fiver is a fiver

In 1.1, every Dog was its own creature. Two dogs called Rex were two dogs. That's right for dogs. When you ask "is this my dog?", you mean this exact one.

Money isn't like that. Five pounds in your pocket and five pounds in mine are the same amount. Asking "is it the same note?" is a question nobody cares about.

Think of it like this

Remember the two twenty-pound notes from 1.1? "Is it the same note?" and "is it worth the same?" were two different questions. For a dog, the first question is the one that matters. For money, only the second one ever does. Nobody has ever refused a twenty because they wanted the other twenty.

An object that stands for a value like that is a value object. Two of them with the same contents are interchangeable. Swap one for the other and nobody could ever tell. You've used value objects without the name. String is one, and so is 1.1's Integer.

What goes inside a Money

Before Money can compare anything, we have to decide what it holds. The tempting answer is a double. Four pounds ninety-nine is 4.99, after all. You already know why not, because 0.2 caught double lying. src/TenCoins.java reruns 0.2's experiment. It adds a 10p coin ten times, two ways. A double counts it as 0.10 of a pound. A long counts it as 10 pennies.

what actually happens (verified, JDK 17)
$ java TenCoins
as a double: 0.9999999999999999
as a long:   100

Ten 10p coins, and the double till says you're short. The long till says exactly 100. 0.2 gave you the rule, and now we obey it: never use double for money. Count whole coins instead.

So a Money holds two things. First, the amount, counted in the smallest coin of its currency. That's pennies for pounds, cents for dollars, paise for rupees. The general name for that smallest coin is a minor unit. Four pounds ninety-nine is 499 minor units.

Second, which money it is. The number 500 means nothing alone. 500 pennies is a fiver. 500 cents is five dollars. So every Money carries a currency code too — "GBP" for pounds, "USD" for dollars, "INR" for rupees. Those three-letter codes are the ones banks use.

Here's the first Money. Open src/Lab1_DefaultEquals.java.

src/Lab1_DefaultEquals.java — the Money class
static class Money {
    long minorUnits;
    String currency;

    Money(long minorUnits, String currency) {
        this.minorUnits = minorUnits;
        this.currency = currency;
    }
}

Line 1 opens the class. static in front of class is the word 1.1 asked you to ignore. It lets main build a Money without first building a whole Lab1_DefaultEquals object. Take it off and javac refuses to compile main. Section 6 finally explains it, with that error on your screen.

Line 2 is a field holding the amount. It's a long, not an int. 0.2 said to use long whenever a count could pass two billion. Two billion pennies is only twenty million pounds, and a bank passes that before breakfast.

Line 3 is a field holding the currency code, as text. Lines 5 to 8 are a constructor exactly like Dog's in 1.1. Each this. line copies one handed-in value into the object's own field.

src/Lab1_DefaultEquals.java — inside main
Money a = new Money(500, "GBP");
Money b = new Money(500, "GBP");

System.out.println("A: " + (a == b));
System.out.println("B: " + a.equals(b));

Lines 1 and 2 build two fivers, separately, with new each time. Line 4 asks ==, which you can answer from 1.1. Line 5 asks .equals.

Here's the catch. Look hard at the class before you guess. Nobody wrote a method called equals in it.

Predict before you run — Lab 1

Two lines, A and B. A is a warm-up. For B: does this even compile, with no equals written anywhere? And if it does, what does it say?

what actually happens (verified, JDK 17)
$ java Lab1_DefaultEquals
A: false
B: false

It compiled. And B is false. Two fivers, same amount, same currency, and Java says they aren't equal.

If you guessed true, that was good reasoning from 1.1, where .equals compared contents. But the authors of String and Integer wrote it to. Nobody wrote it for Money.

So where did .equals come from at all? Every class you write quietly gets a few starter methods from Java. You don't ask for them. .equals is one. The starter version asks exactly what == asks: same object? That's why B gave the same answer as A.

The starter methods come from a family tree that every class belongs to. That tree is the whole subject of 1.3. For today, one honest sentence is enough. Until you replace it, every object's .equals means "same object".

Going deeper The starter equals, in Java's own source

The starter methods live in a class called Object, which sits at the top of every class's family tree. Here's its equals, copied from the JDK 17 source on this machine (java/lang/Object.java).

JDK 17 source — java/lang/Object.java
public boolean equals(Object obj) {
    return (this == obj);
}

That's the whole thing. Each object is equal only to itself. It's a dog-shaped answer, handed to every class, even the ones that are money.

Worth remembering

Section 2Teaching Money what equal means

Time to replace the starter method with your own. This is it, from src/Lab2_Equals.java. It sits inside the Money class, under the constructor.

src/Lab2_Equals.java — Money's own equals
public boolean equals(Object other) {
    if (this == other) {
        return true;
    }
    if (!(other instanceof Money)) {
        return false;
    }
    Money that = (Money) other;
    return this.minorUnits == that.minorUnits
        && this.currency.equals(that.currency);
}

Eleven lines, and several words in them are new. So let's take them slowly.

Line 1 has four parts. public is 0.4's word for "anybody may use this". It's required, because the starter version is public and a replacement can't be less open. boolean means it hands back true or false. equals is the exact name. Get one letter wrong and you've written a new method that nobody ever calls.

Then the parameter, (Object other). Object is Java's word for "any object at all" — a Money, a String, a Dog. It must be Object, because a caller might hand in anything. Write Money there instead and it still compiles, but a HashSet then skips your method entirely. The "Going deeper" box just after Lab 2 shows that happening.

That mistake has a guard, and you'll see it in the finished Money. Write @Override on the line above a method. It tells javac: this replaces a starter method, so refuse to compile if it doesn't. Without it, a wrong parameter or a typo compiles silently. Then the starter method keeps running. Where the starter methods come from, and what "replacing" one really means, is 1.3.

Lines 2 to 4 give this a second job, as 1.1 promised. Until now this always came with a dot: this.name meant "my own field". With no dot, this means the whole object whose equals is running. So this == other asks: did someone hand me myself? Then it's certainly equal, and we can stop early.

Lines 5 to 7 are a safety check. instanceof asks: is the object this ticket finds a Money? null isn't any kind of object, so for null the answer is no as well. The ! in front is 0.3's "not", and the brackets make it apply to the whole question. So line 5 reads "if this is not a Money". Then we hand back false.

Line 8 is the odd one. (Money) other is a cast, the same brackets as (int) 3.99 from 0.2. This cast changes nothing about the object. It tells javac: "this ticket finds a Money, trust me." Line 5 earned that trust. Now that holds the same ticket, typed as Money. That matters, because other.minorUnits won't compile and that.minorUnits will.

Lines 9 and 10 are the actual decision. Compare the amounts with ==, which is safe on a plain long. Compare the currencies with .equals, because they're text. 1.1 taught you never to use == on text. && means both must be true.

src/Lab2_Equals.java — inside main
Money a = new Money(500, "GBP");
Money b = new Money(500, "GBP");
Money c = new Money(500, "USD");

System.out.println("C: " + a.equals(b));
System.out.println("D: " + a.equals(c));
System.out.println("E: " + a.equals(null));
System.out.println("F: " + a.equals("500 GBP"));

a and b are five pounds, c is five dollars. Then four questions. Is a equal to b? To c? To null? To text that says 500 GBP? The bet is E, because in 1.1 a dot near null crashed the program.

Predict before you run — Lab 2

Four answers, C to F. For E: does handing null to equals crash, like 1.1's NullDog did? And for F, does text that says "500 GBP" count as money?

what actually happens (verified, JDK 17)
$ java Lab2_Equals
C: true
D: false
E: false
F: false

C is true at last. Same amount, same currency, so equal — even though a and b are two different objects. That's the whole point of a value object.

D is false, and good. Five pounds and five dollars are not the same money.

E didn't crash. Look where the dot is. It's on a, which holds a real ticket. null only went in as the thing being compared. Inside, line 5 of equals asked null instanceof Money and got false. Your method handled the empty pocket politely. The crash would need a dot on the empty pocket, like m.equals(a) with m holding null. That's 1.1's rule exactly.

F is false. Text that describes money is still text. instanceof turned it away at the door.

Going deeper The equals that looks right and isn't — and the word that catches it

Here's a version almost everyone writes once. It takes a Money instead of an Object, so it needs no instanceof and no cast. It's in src/WrongEquals.java.

src/WrongEquals.java — the tempting version
public boolean equals(Money that) {
    return this.minorUnits == that.minorUnits
        && this.currency.equals(that.currency);
}

It compiles. Call it yourself and it works. Then hand the same fivers to a HashSet, which Section 3 introduces properly.

what actually happens (verified, JDK 17)
$ java WrongEquals
you calling it: true
the set asking: false

Remember overloading from 0.4? One name, several parameter lists, and Java picks by what's in the brackets. That's what happened here. You didn't replace the starter equals(Object). You added a second equals(Money) beside it. Your own code passes a Money, so it gets your version. The set holds its items as plain Objects, so it calls equals(Object) — the starter one, still sitting there.

That's the mistake @Override guards against. In WrongEquals.java, take the // off the line // @Override and compile.

what actually happens (verified, JDK 17)
$ javac WrongEquals.java
WrongEquals.java:20: error: method does not override or implement a method from a supertype
        @Override
        ^
1 error

"Supertype" means the place the starter method came from. The silent bug became a compile error, which is why the finished Money.java has @Override above both replacements.

Going deeper The five promises every equals makes

The documentation for Object.equals lists rules every replacement must keep. For any x, y and z that aren't null:

  1. Everything equals itself. x.equals(x) is true. The proper name is reflexive.
  2. Equality works both ways round. If x.equals(y), then y.equals(x). That's symmetric.
  3. Equality passes along a chain. If x equals y and y equals z, then x equals z. That's transitive.
  4. The answer doesn't wobble. Ask twice, get the same answer, unless a compared field changed. That's consistent.
  5. Nothing equals null. x.equals(null) is false. Lab 2's line E checked it.

The both-ways rule is the one people break. Suppose Money said yes to the text "500 GBP". Then "500 GBP".equals(a) still asks String's method, which says no. Collections misbehave once equality is lopsided. So only ever say yes to your own type.

A teammate's equals compares currencies with this.currency == that.currency. Every test passes. What do you say?

Worth remembering

Section 3The set that can't find your fiver

Your equals works. So let's use Money somewhere real.

Java's toolbox has a HashSet. It holds each value at most once. And it answers "is this in here?" very fast, even with a million things inside. Its sibling is a HashMap, which stores pairs. You look up one thing, the key, and get back another, the value. It's your phone's contacts: look up a name, get a number.

You'll build both from scratch in Phase 2, and reach for them constantly in Phase 2.5. Today you're only a customer. Lab 3 starts with two lines above the class.

src/Lab3_LostInASet.java — the top of the file
import java.util.HashMap;
import java.util.HashSet;

You met import in 0.3, with Scanner. It says: I'll be using this tool from Java's toolbox, so go and find it. Leave these out and javac says it cannot find the symbol HashSet. Now main.

src/Lab3_LostInASet.java — inside main
HashSet<Money> seen = new HashSet<>();
seen.add(new Money(500, "GBP"));
seen.add(new Money(500, "GBP"));
System.out.println("G: the set holds " + seen.size());
System.out.println("H: " + seen.contains(new Money(500, "GBP")));

HashMap<Money, String> names = new HashMap<>();
names.put(new Money(500, "GBP"), "a fiver");
System.out.println("I: " + names.get(new Money(500, "GBP")));

First, the angle brackets, because you have to type them. HashSet<Money> means "a set that holds Money and nothing else". Try to add a String and javac refuses. The empty <> on the right means "same as the left". Leave the brackets out and the set holds plain Objects, so nothing stops a String getting in. The full story is 1.4.

Line 1 makes an empty set called seen. Lines 2 and 3 call add, which puts a value into the set unless an equal one is inside. They add two fivers, built separately, and your equals says they're the same value. Line 4 calls size(), which counts what the set holds. Line 5 calls contains, which answers yes or no: is a value equal to this one inside? It asks about a third fiver, built fresh.

Line 7 makes a map whose keys are Money and whose values are text. Line 8 calls put, which stores a value under a key — here the text "a fiver" under one fiver. Line 9 calls get, which hands back the value stored under a key, or null if there isn't one. It looks up another fiver.

This is the most important prediction in the chapter. Professionals get it wrong in real code, and the fix is a single method. Your equals is correct. Hold on to that while you guess.

Predict before you run — Lab 3

Three lines, G, H and I. How many fivers does the set hold? Does it contain the third one? And what does the map hand back?

what actually happens (verified, JDK 17)
$ java Lab3_LostInASet
G: the set holds 2
H: false
I: null

The set holds two fivers. A set that keeps each value once has kept the same value twice. It can't find a third fiver. And the map, asked with a different fiver, hands back null — 1.1's empty pocket.

Your equals isn't wrong. Lab 2 proved it. The set never asked it. To see why, you need to know how a set is so fast.

How a set finds things fast

Think of it like this

A big office post room has a wall of pigeonholes, one for each letter of the alphabet. A letter for Patel goes in P. When Patel asks "any post for me?", nobody searches the whole room. They look in P, and only P. That's why it's fast. But suppose someone filed Patel's letter under K. The answer is "nothing for you", while the letter sits in the building.

A HashSet works like that post room, except its pigeonholes are numbered. Before it stores or looks for anything, it asks the object for a number. That number is the object's hash code, and it comes from a method called hashCode. It's another starter method, so every object has one. The number decides the pigeonhole. Then the set only compares against things in that pigeonhole — and only there does it call your equals.

The starter hashCode hands back a number tied to that one object. Think of it as the note's serial number. Two fivers built separately get two different serial numbers. Step through what that does to Lab 3.

Why the set can't find your fiver

What you did

add fiver #1

The set's pigeonholes

3 fiver #1
every other pigeonhole empty

Step 1 — seen.add(fiver #1). The set asks fiver #1 for its hash code. The starter hashCode gives fiver #1's serial number, which points at pigeonhole 3. The pigeonhole is empty, so in it goes. The pigeonhole numbers in this picture are made up. Real ones come from much bigger numbers.

What you did

add fiver #1
add fiver #2

The set's pigeonholes

3 fiver #1
11 fiver #2

Step 2 — seen.add(fiver #2). A different object, so a different serial number, so pigeonhole 11. The set looks in 11, finds it empty, and concludes this fiver is new. It never looked in pigeonhole 3. Your equals was never called. The set now holds two equal fivers.

What you did

contains fiver #3 ?

The set's pigeonholes

3 fiver #1
11 fiver #2
6 (empty) — "not here"

Step 3 — seen.contains(fiver #3). A third serial number, a third pigeonhole, and it's empty. So the answer is false. Two equal fivers are in the set, and the set honestly can't see either of them. The map's null is the same story with a key.

What you did — with hashCode written

add fiver #1
add fiver #2 → equal, refused
contains fiver #3 → true

The set's pigeonholes

5 fiver #1
every other pigeonhole empty

Step 4 — the fix. Now give Money a hashCode worked out from its amount and currency. All three fivers produce the same number, so all three go to pigeonhole 5. The set looks there, finds fiver #1, and now calls your equals. Equal, so fiver #2 is refused and fiver #3 is found.

Writing the hashCode

The fix is already in the lab file, switched off between a /* line and a */ line. Everything between those two markers is ignored, like one long comment.

src/Lab3_LostInASet.java — Money's hashCode, switched off
public int hashCode() {
    return 31 * Long.hashCode(minorUnits) + currency.hashCode();
}

Line 1: public for the same reason as equals. int because a hash code is one whole number.

Line 2 builds that number from the two fields. Long.hashCode(minorUnits) squashes the 64-switch long into a 32-switch int. Long with a capital L is the class version of long, like Integer in 1.1. Notice it's called on the class name, not on an object. Remember that shape for Section 6.

currency.hashCode() asks the text for its own hash code. String works one out from its letters, so "GBP" always gives the same number. 31 * mixes the two numbers, so different Moneys rarely land on the same total. Why 31 is in the box below.

Now switch it on. Delete the /* line and the */ line, save, and run Lab 3 again.

what actually happens (verified, JDK 17)
$ javac Lab3_LostInASet.java
$ java Lab3_LostInASet
G: the set holds 1
H: true
I: a fiver

One fiver in the set. The third fiver is found. The map hands back the name. Three lines of code fixed all three, and none of them touched equals.

The rule

If two objects are equal, their hash codes must be equal. That's the equals–hashCode contract. It only runs one way. Two unequal objects may share a hash code — a collision, like two surnames starting with P. That costs a little speed, never a wrong answer. So in practice: write equals and hashCode together, from exactly the same fields.

Going deeper Why 31, and the lazy hashCode that's legal and terrible

Why 31? It's odd, so multiplying by it loses no information. It's prime, which helps results spread out. String.hashCode uses it on each letter. And 31 * x equals (x << 5) - x, a shift and a subtract, which once mattered for speed. Mostly it's tradition.

The lazy one. return 42; keeps the contract, because everything gets 42. But every item now lands in one pigeonhole, and your instant lookup becomes a walk down a list. Legal, and terrible.

Sharing a pigeonhole doesn't save you. A HashMap stores each key's full hash code beside it, and compares those numbers before calling equals. JDK 17's HashMap.getNode checks first.hash == hash first. So equal objects with different hash codes miss each other even in the same pigeonhole.

Worth remembering

Section 4Change it, and the set loses it

The set behaves now. So here's the next attack. The hash code is worked out from the fields. What if a field changes after the set has filed it?

src/Lab4_ChangedKey.java — inside main
HashSet<Money> seen = new HashSet<>();
Money m = new Money(500, "GBP");
seen.add(m);
System.out.println("J: " + seen.contains(m));

m.minorUnits = 999;   // change it while it sits in the set

System.out.println("K: " + seen.contains(m));
System.out.println("L: " + seen.contains(new Money(500, "GBP")));
System.out.println("M: the set holds " + seen.size());
for (Money each : seen) {
    System.out.println("N: is it m? " + (each == m));
}

Lines 1 to 3 make a set, build one fiver into m, and add it. Line 4 checks the set can find it. Then line 6 follows m's ticket and writes 999 into the object's field. There's a dot, so that change happens on the heap, to the very object the set is holding.

Line 8 asks the set about m — the same ticket, the same object. Line 9 asks about a fresh fiver, which is what m used to be. Line 10 asks how many things the set holds. Lines 11 to 13 use the for loop from 0.5 to visit everything in the set. For each item, it asks ==: is this the very same object as m?

Here's the bet. K asks the set whether it contains an object that it is literally holding.

Predict before you run — Lab 4

Five lines, J to N. The one to think hard about is K. Same ticket, same object, and nobody took it out of the set. Can the set find it?

what actually happens (verified, JDK 17)
$ java Lab4_ChangedKey
J: true
K: false
L: false
M: the set holds 1
N: is it m? true

There's the promise from the start of the chapter. The set holds one thing, that thing is m, and the set says it doesn't contain m.

Back to the post room. The set filed m in the pigeonhole for "500 GBP". Then you changed the amount, and the hash code comes from the amount. So K looked in a 999 pigeonhole, and found nothing.

L went the other way. A fresh fiver points at the pigeonhole m is sitting in. The set found m there and called equals. Is 999 equal to 500? No.

The object is lost inside the set. You can't find it, so you can't remove it either. It's in there for good, and N proves it.

The fix isn't a cleverer set. It's a Money that can't change. In 0.6, nothing you did to a String could change it. toUpperCase built a new String and left the old one alone. Money should work the same way. An object that can't be changed after it's built is immutable.

Two words make it so, and you've met both.

private — 0.4 said it would earn its keep "when you have classes with parts worth keeping private". Here we are. A private field can only be touched by code inside its own class. Nobody outside can write m.minorUnits = 999.

final — in 0.2 it "glued the box shut". On a field, the constructor fills it exactly once, and nothing can write to it again. Not even code inside the class.

Here's the start of the finished src/Money.java. It's a file of its own now, because the rest of the chapter uses it.

src/Money.java — the fields, the constructor and the getters
private final long minorUnits;
private final String currency;

public Money(long minorUnits, String currency) {
    if (currency == null) {
        throw new IllegalArgumentException("a Money needs a currency");
    }
    this.minorUnits = minorUnits;
    this.currency = currency;
}

public long minorUnits() {
    return minorUnits;
}

public String currency() {
    return currency;
}

Lines 1 and 2 are the same two fields, now private final. Line 4 opens the constructor. It's public so that code in other files can build Money.

Lines 5 to 7 are a guard at the door. Try to build a Money with no currency, and throw stops the program right there with that message. Section 7 is all about that word. Lines 8 and 9 fill the two fields, once each. final allows exactly that.

Lines 12 to 18 are two small methods. The fields are private, so outside code needs some way to read them. A method whose only job is to hand back a field is a getter. Notice what's missing. There's a way to read each field, and no way to write one. That absence is the whole design.

The file also has your equals and hashCode, each with @Override on the line above. That's Section 2's guard: javac refuses to compile if the method doesn't really replace a starter one.

Attack it from outside, then from inside

src/TryToChange.java builds a fiver and has one line switched off with //. That line is the Lab 4 attack, word for word. Take the // off and compile.

what actually happens (verified, JDK 17)
$ javac TryToChange.java
TryToChange.java:9: error: minorUnits has private access in Money
        fiver.minorUnits = 999;   // the attack: take the // off this line
             ^
1 error

private stopped it. The attack that lost money in Lab 4 can't even be compiled now, so the bug can never run.

What about code inside Money? private lets that in. So Money.java has a switched-off line too, a method that tries to wipe the amount. Put the // back in TryToChange.java. Then take the // off the void wipe() line in Money.java and compile.

what actually happens (verified, JDK 17)
$ javac Money.java
Money.java:39: error: cannot assign a value to final variable minorUnits
    void wipe() { minorUnits = 0; }   // take the // off this line to see final at work
                  ^
1 error

That's the very same error 0.2's Finals.java gave you. private keeps strangers out. final keeps you out, including the version of you who edits this file in six months. Put the // back before you go on.

So how do you add money?

If a Money can never change, how do you add to one? The way String does it. Leave the old one alone, build a new one, and hand it back.

src/Money.java — plus
public Money plus(Money other) {
    if (!this.currency.equals(other.currency)) {
        throw new IllegalArgumentException(
            "cannot add " + other.currency + " to " + this.currency);
    }
    return new Money(this.minorUnits + other.minorUnits, this.currency);
}

Line 1 says plus takes a Money and hands back a Money. Lines 2 to 5 check the two currencies match, and refuse loudly if they don't. That's the guard Section 7 attacks. Line 6 is the real work. It adds the two amounts, builds a brand new Money with the total, and returns it. this and other are never touched.

So a.plus(b) on its own line does nothing useful, exactly like s.toUpperCase() did in 0.6. You have to catch what it hands back: Money total = a.plus(b);.

Judgment call. Someone says: "Making Money immutable is wasteful. Every plus builds a new object. Let plus change the amount in place." What's the strongest reply?

Worth remembering

Section 5A final field that changes anyway

Money's fields are a long and a String. Both are safe. A long is a plain number, and nobody can change a String. But what if a private final field held something that can change? An array, say. An array variable holds a ticket, not the array.

src/Lab5_SplitBill.java records how a bill is split between friends, one share of pennies per slot. It has two versions of the class side by side. Here's the first.

src/Lab5_SplitBill.java — LeakySplit
static class LeakySplit {
    private final long[] shares;

    LeakySplit(long[] shares) {
        this.shares = shares;
    }

    long total() {
        long sum = 0;
        for (long s : shares) {
            sum = sum + s;
        }
        return sum;
    }
}

Line 2 is one field, private final, holding an array of longs. Lines 4 to 6 are the constructor. It takes an array and stores it in the field. Lines 8 to 14 add up every share with 0.5's for loop and hand back the total.

The second version, SafeSplit, is the same except for its constructor.

src/Lab5_SplitBill.java — SafeSplit's constructor
SafeSplit(long[] shares) {
    this.shares = new long[shares.length];
    for (int i = 0; i < shares.length; i++) {
        this.shares[i] = shares[i];
    }
}

Line 2 builds a brand new array, as long as the one handed in, and stores that in the field. Lines 3 to 5 copy each number across, slot by slot.

src/Lab5_SplitBill.java — inside main
long[] amounts = {1000, 1000, 1000};
LeakySplit leaky = new LeakySplit(amounts);
SafeSplit safe = new SafeSplit(amounts);
System.out.println("O: leaky total " + leaky.total());
System.out.println("P: safe total  " + safe.total());

amounts[0] = 0;   // the caller changes THEIR array afterwards

System.out.println("Q: leaky total " + leaky.total());
System.out.println("R: safe total  " + safe.total());

Line 1 is a £30 bill split three ways, ten pounds each. Lines 2 and 3 build both kinds of split from that same array. Lines 4 and 5 print both totals. Then line 7 changes the first slot of amounts — the caller's own array — to zero. Lines 9 and 10 print both totals again.

Predict before you run — Lab 5

Four totals, O to R. Both classes have a private final field. Nobody outside can touch it, and nothing can re-assign it. So does Q change when the caller edits their own array?

what actually happens (verified, JDK 17)
$ java Lab5_SplitBill
O: leaky total 3000
P: safe total  3000
Q: leaky total 2000
R: safe total  3000

Q dropped to 2000. A private, final field, and its contents changed anyway. Someone got a free dinner.

Draw it, the way 1.1 taught you. this.shares = shares; copied a ticket. Two tickets, the caller's and the field's, find one array. final glued the field's box shut, so it always holds that ticket. But amounts[0] = 0 follows the caller's ticket to the same array, and writes in it.

SafeSplit built its own array and copied the numbers in. Nobody else holds a ticket to it. So R stays at 3000.

Stack — main's plate

amounts = → array #1
leaky = → LeakySplit
safe = → SafeSplit

Heap — after amounts[0] = 0

LeakySplit shares → array #1
array #1 0 · 1000 · 1000
SafeSplit shares → array #2
array #2 1000 · 1000 · 1000

Count the arrows into array #1. Two — one from main, one from inside LeakySplit. That second arrow is the leak. Array #2 has exactly one arrow into it, and it's from inside SafeSplit.

Copying something you were handed before you keep it is called a defensive copy. The point is that nobody else ends up holding a ticket to your insides.

final is not immutable

final is a promise about the box. The box will always hold the same ticket. It says nothing about the object that ticket finds. A final field holding an array is only as safe as your copy makes it.

The leak on the way out

There's a second door, and it's easy to miss. Suppose SafeSplit had a getter that handed back its field.

a leaky getter — don't write this
long[] shares() {
    return shares;
}

Its one real line hands out a ticket to the private array. The caller can write in it, and your careful copy in the constructor was for nothing. So the real SafeSplit in your src/ copies on the way out too. Its shares() builds a fresh array, copies every share in, and hands back the copy. It's the constructor's trick, run the other way.

Worth remembering

Section 6static, finally

You've typed static in every program since 0.1. 0.2 gave it a one-line meaning in a box. 0.4 said "the full story lands in chapter 1.2". 0.7 said it means "this method does not belong to a bowl". And 1.1 told you to ignore it on static class Dog. That's five chapters of IOUs. This is the payment.

Think of it like this

In a classroom, every student has their own pencil case. Thirty students, thirty pencil cases, and what's in one has nothing to do with the others. But the room has one clock, on the wall. Nobody brings their own. Ask "what time is it?" and everyone reads the same clock. It belongs to the room, not to any student.

A field without static is a pencil case. Every object gets its own box, inside itself, as 1.1 drew. A field with static is the clock. There's exactly one box, it belongs to the class, and no object carries it.

So static means "belongs to the class, not to any one object". Here it is in src/Lab6_Static.java, beside a field without it.

src/Lab6_Static.java — the Money class
static class Money {
    static int made = 0;
    int myNumber;
    long minorUnits;
    String currency;

    Money(long minorUnits, String currency) {
        this.minorUnits = minorUnits;
        this.currency = currency;
        made = made + 1;
        myNumber = made;
    }
}

Line 2 is the clock. static int made is one box for the whole Money class, starting at 0. Line 3 is a pencil case. int myNumber is one box inside every Money object. Lines 4 and 5 are the familiar fields, also one per object.

The constructor fills the usual two fields. Then line 10 adds one to the shared count. Line 11 copies the count as it is right now into this object's own myNumber. So each Money remembers which one it was.

src/Lab6_Static.java — inside main
Money a = new Money(500, "GBP");
Money b = new Money(200, "GBP");
Money c = new Money(100, "USD");

System.out.println("S: Money.made is " + Money.made);
System.out.println("T: a is number " + a.myNumber);
System.out.println("U: c is number " + c.myNumber);
System.out.println("V: a.made is " + a.made);

Lines 1 to 3 build three Moneys. Line 5 reads the shared box. Notice it says Money.made, naming the class, because that's who owns the box. Lines 6 and 7 read two objects' own numbers. Line 8 does something a little sneaky. It reads made through a, as if it were a's own field.

Think about V carefully. a was built first, back when made was only 1.

Predict before you run — Lab 6

Four lines, S to V. S, T and U follow from the picture of one clock and three pencil cases. For V: a was made when the count was 1. So what is a.made?

what actually happens (verified, JDK 17)
$ java Lab6_Static
S: Money.made is 3
T: a is number 1
U: c is number 3
V: a.made is 3

V is 3, not 1. If you said 1, you pictured made inside a, as a pencil case. It isn't there. There is only one made box, the clock on the wall. a.made is Java letting you read the clock while pointing at a student. It's legal and misleading. Always write Money.made, so the code says what's true.

T is 1 because myNumber is a pencil case. Each object copied the count into its own box when it was built, and that box kept its value.

Stack — main's plate

a = → Money #1
b = → Money #2
c = → Money #3

Heap — one box per object

Money #1 myNumber = 1 · 500 GBP
Money #2 myNumber = 2 · 200 GBP
Money #3 myNumber = 3 · 100 USD

The class Money — one box, full stop

made = 3

Three objects, three myNumber boxes, one inside each. One made box, and it isn't inside any of them. That's the whole meaning of static.

A static method has no "this"

Methods work the same way. A method without static belongs to one object, so it has a this and can read that object's fields. A static method belongs to the class. There's no this at all.

So what happens if a static method asks for an object's field? Inside Lab 6's Money there's one line switched off.

src/Lab6_Static.java — switched off inside Money
// static void report() { System.out.println(myNumber); }   // whose number?

It's a static method that tries to print myNumber. Take the // off the front of the line and compile.

what actually happens (verified, JDK 17)
$ javac Lab6_Static.java
Lab6_Static.java:22: error: non-static variable myNumber cannot be referenced from a static context
        static void report() { System.out.println(myNumber); }   // whose number?
                                                  ^
1 error

0.4 quoted this error with a method in it instead of a variable. Now you can read every word. "Non-static variable" means a pencil case. "Static context" means a method with no student in it. There are three Moneys, each with its own myNumber. Whose number did you want? The method has no way to say, so Java refuses to guess. Put the // back afterwards.

And that's why main has to be static. When you type java Lab6_Static, no objects exist yet. Not one. So the first method to run must belong to the class.

When you'd write static yourself

Most methods you write from now on won't be static. plus, equals and the getters all need a particular Money. But some jobs belong to the class. Here's one, from src/Money.java.

src/Money.java — a static method
public static Money zero(String currency) {
    return new Money(0, currency);
}

Line 1: anyone may call it, it belongs to the class, and it hands back a Money. Line 2 builds an empty Money in the currency you asked for.

Why static? Because its job is to make a Money. If you needed a Money in hand to call it, you'd need a Money to get a Money. So you call it on the class name: Money.zero("GBP"). Attack 4 in the next section starts a running total that way.

You've called static methods before without the word. Long.hashCode(...) in Section 3 was one. Math.round(3.99) from 0.2 was another. Neither needs an object. They take what you hand them and give back an answer.

One more shape: static final together. One box, belonging to the class, glued shut. That's a shared constant, like Math.PI.

And static class, from 1.1

That leaves the word 1.1 told you to ignore, in static class Dog. A class written inside another class is a nested class. Without static, each nested object belongs to one object of the outer class. Java gives it a hidden link to that object. main is static, so there's no outer object to link to. Take static off class Money in Lab 6 and compile.

what actually happens (verified, JDK 17)
$ javac Lab6_Static.java
Lab6_Static.java:26: error: non-static variable this cannot be referenced from a static context
        Money a = new Money(500, "GBP");
                  ^
… the same error again for b and c …
3 errors

The same "static context" complaint, once per new Money(...). The this it wants is the outer object's, the hidden link. static class says "no outer object needed", which is why every class tucked inside a lab file carries the word. Put static back afterwards.

Worth remembering

Section 7Pounds plus dollars, and the full attack

One attack is left, and it's the one this whole type was built for. What should five pounds plus three dollars be?

Not eight. Eight what? Any number plus handed back would be a lie that looks like a right answer. It would travel quietly into someone's bank statement.

That's the worst kind of bug. 0.2's int overflow was this kind, wrong with no error message. So Money's rule is the opposite. If the question has no right answer, stop the program and say why.

Here are those lines from plus again.

src/Money.java — the guard inside plus
if (!this.currency.equals(other.currency)) {
    throw new IllegalArgumentException(
        "cannot add " + other.currency + " to " + this.currency);
}

Line 1 checks whether the two currency codes don't match. The ! is 0.3's "not" again. Lines 2 and 3 are one statement over two lines, and it's your first throw.

throw stops the method on the spot and sends a message out of it. Unless something catches it, the whole program stops with a stack trace. That's the same stop as Boom's divide by zero in 0.4. This time you chose the moment and wrote the message. IllegalArgumentException is Java's ready-made stop for "you handed me a bad argument". new builds one with your text inside, so it's an object too.

Without those lines, five pounds plus three dollars would come back as 800 GBP, looking completely fine. Catching a thrown message, and its different kinds, is all of chapter 1.5. Today you only throw.

src/PoundsPlusDollars.java — inside main
Money lunch = new Money(450, "GBP");
Money drink = new Money(150, "GBP");
Money total = lunch.plus(drink);
System.out.println("lunch and drink: " + total.minorUnits() + " " + total.currency());

Money souvenir = new Money(300, "USD");
Money nonsense = total.plus(souvenir);
System.out.println("and a souvenir:  " + nonsense.minorUnits() + " " + nonsense.currency());

Lines 1 to 3 buy lunch and a drink in pounds and add them up. Line 4 prints the total using the two getters. Line 6 buys a souvenir in dollars. Line 7 tries to add it to the pound total. Line 8 would print the result.

Predict before you run — pounds plus dollars

Does the first line print? What happens at line 7 — and does line 8 ever print? If there's a stack trace, which method is on the top at line?

what actually happens (verified, JDK 17)
$ java PoundsPlusDollars
lunch and drink: 600 GBP
Exception in thread "main" java.lang.IllegalArgumentException: cannot add USD to GBP
	at Money.plus(Money.java:33)
	at PoundsPlusDollars.main(PoundsPlusDollars.java:13)

The pounds added up fine: 600 GBP. Then the program stopped, and the second line never printed.

Read the trace the way 0.4 taught you. The message first: cannot add USD to GBP, your own text. Then the top plate: Money.plus, line 33, the throw. Then who called it: main, line 13, the total.plus(souvenir) line.

It feels like failure. It's the opposite. A clear message on the exact line beats a wrong number nobody notices for a month.

The constructor's guard works the same way. A Money with a null currency is stopped at the door. So every Money that exists has a currency, and no method needs to check again.

The full attack

src/Attacks.java throws everything this chapter found at the finished Money. Each attack prints true if Money survived it. The last one is meant to stop the program, and that's surviving too.

src/Attacks.java — the attacks, comments trimmed
Money a = new Money(500, "GBP");
Money b = new Money(500, "GBP");

System.out.println("1 equal amounts are equal:     " + a.equals(b));
System.out.println("2 equal amounts, equal hash:   " + (a.hashCode() == b.hashCode()));

HashSet<Money> seen = new HashSet<>();
seen.add(a);
System.out.println("3 the set finds an equal copy: " + seen.contains(b));

Money total = Money.zero("GBP");
for (int i = 0; i < 10; i++) {
    total = total.plus(new Money(10, "GBP"));
}
System.out.println("4 ten 10p make exactly 100p:   " + total.equals(new Money(100, "GBP")));

Money sixPounds = a.plus(new Money(100, "GBP"));
System.out.println("5 plus made new, a is unmoved: " + (sixPounds.minorUnits() == 600 && a.minorUnits() == 500));

System.out.println("6 not equal to null or text:   " + (!a.equals(null) && !a.equals("500 GBP")));

System.out.println("7 adding USD to GBP...");
a.plus(new Money(500, "USD"));
System.out.println("7 if you can read this, attack 7 got through");

Attack 1 is Lab 1's question: are two separate fivers equal? Attack 2 checks Section 3's contract, equal objects with equal hash codes. Attack 3 is Lab 3, one fiver in a set and a question about the other.

Attack 4 is 0.2's ten coins, one last time. It starts from Money.zero("GBP"), the static method. It adds a 10p coin ten times, and each total = total.plus(...) catches the new Money. Then it asks whether the total is exactly one pound.

Attack 5 checks that plus made a new Money and left a alone. Attack 6 asks Lab 2's awkward questions, null and text. Each ! turns equals saying no into true. Attack 7 adds dollars to pounds. If Money is right, the line after it never prints.

An eighth attack is missing on purpose. Lab 4's m.minorUnits = 999 can't go in this file, because it won't compile. The best defence is an attack that can't be typed.

Predict before you run — the attack

Which of attacks 1 to 6 print true? And what exactly happens at attack 7 — what's the last line you'll see?

what actually happens (verified, JDK 17)
$ java Attacks
1 equal amounts are equal:     true
2 equal amounts, equal hash:   true
3 the set finds an equal copy: true
4 ten 10p make exactly 100p:   true
5 plus made new, a is unmoved: true
6 not equal to null or text:   true
7 adding USD to GBP...
Exception in thread "main" java.lang.IllegalArgumentException: cannot add USD to GBP
	at Money.plus(Money.java:33)
	at Attacks.main(Attacks.java:40)

Six trues and one loud refusal. Each line is a bug you watched happen in this chapter, not a rule you were told. (Line 40 is the USD line in the full file, which has comments.)

Attack 4 deserves a moment. That's the exact sum that came out as 0.9999999999999999 at the top of the page. In minor units it's exactly 100, and equals agrees.

Going deeper Java can write all of this for you — and when it can't

Since Java 16 there's a shorter way to declare a value object, a record. One line gives you private final fields, a constructor, getters, equals, hashCode and toString. It's in src/RecordMoney.java.

src/RecordMoney.java — the whole Money
record Money(long minorUnits, String currency) { }
what actually happens (verified, JDK 17)
$ java RecordMoney
true
true
Money[minorUnits=500, currency=GBP]

Equal, found in a set, and printed readably. That third line is the starter toString method, replaced for you. The starter one prints a class name and a number.

So why write it by hand? Because now you know what a record generates, and why. And a record can't do everything. It makes no defensive copies, so a record holding an array leaks like LeakySplit. It knows nothing about currencies, so plus and its guard are still yours.

One honest limit of our Money. Our examples assume 100 minor units to the pound or dollar. The Japanese yen has no minor unit, and a few currencies use 1000. java.util.Currency knows each one's real number. For fractions of a minor unit, like interest, real systems use java.math.BigDecimal.

Worth remembering
Your turn

Everything is in this chapter's src/ folder. Compile it all once with javac *.java, then run each program. A few steps ask you to take a // off a line. Put it back afterwards, or the next javac *.java will stop on it. Predict on paper before every run. A wrong prediction points straight at one wrong idea, and now you know which one.

Section 8Check yourself

Your class has a correct equals, but you forgot hashCode. You put one object in a HashSet, then ask contains about an equal copy. What happens?

A class has the field private final String[] tags;, and its constructor does this.tags = tags;. Is an object of this class immutable?

Judgment call. You're adding a method to Money that converts it to another currency, given a rate. Should it be static?

Section 9What broke, and what surprised you

Write this down in your own words before moving on. The predictions you got wrong are the useful ones. Each points at one belief you've now replaced.

Worth answering properly, on paper:

  1. Which predictions did you get wrong? For each one, what were you picturing instead?
  2. In one sentence: why did Lab 3's set hold two equal fivers?
  3. Lab 4's set held m and said it didn't. Explain that to someone using the post room.
  4. Why isn't final enough to make LeakySplit safe? Draw the arrows.
  5. What does static mean, in your own words — and why does main need it?
Next up

You now have a type that knows what equal means, can't be changed, and refuses nonsense. Chapter 1.3 answers the question this chapter kept postponing: where do starter methods like equals come from? And in 1.7 you'll build a JSON library out of value objects like this one. final fields, equals, hashCode: all of it.

Nice. Chapter marked complete.