Phase 1 · Objects

The compiler reads your angle brackets, then throws them away

By the end of this page you'll know what the angle brackets you've been copying actually do, where they stop protecting you, and why a well-made getter asks you for Circle.class.

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

Write SUGAR on a jar. On Monday your flatmate fills it with salt, because salt and sugar look the same. Nothing happens on Monday. On Saturday you bake your gran's birthday cake, and that's when you find out. The mistake happened on Monday, in one room. The damage happened on Saturday, in another. A label can't stop anybody. This chapter is about a label that can.

Since Chapter 1.2 you've been typing angle brackets. HashSet<Money>. new HashSet<>(). 1.2 gave you one honest sentence about them and promised the full story here. This is it.

Those brackets are a label Java can read. They move the salt mistake from Saturday to Monday. The program refuses to compile on the very line where the salt goes in.

Fair warning about two things coming later. At one point a program will crash on a line with no cast in it. And you'll ask a running program a question. Are a list of words and a list of numbers different kinds of thing? It'll say they're the same. The brackets you typed won't be there any more.

What you'll be able to do

Section 1A jar that holds anything

Start with the jar that has no label. You already have every word you need to build it.

In 1.2 you met Object, Java's word for "any object at all". A String is an Object. So is a Money, a Dog, an Integer. So a field of type Object can hold anything. Here's a jar built that way, from src/Lab1_ObjectJar.java.

src/Lab1_ObjectJar.java — the jar
static class ObjectJar {
    private Object item;

    void put(Object item) {
        this.item = item;
    }

    Object get() {
        return item;
    }
}

Line 1 opens a small class tucked inside the lab file, which is why it says static. 1.2's Section 6 explained that word. Line 2 is the jar's one field, private, of type Object. So it can hold a ticket for any object at all.

Lines 4 to 6 are put. It takes any object and stores its ticket in the field. Lines 8 to 10 are get. It hands that ticket back. Notice its return type is Object too. The jar can't promise anything more, because it doesn't know what's in it.

src/Lab1_ObjectJar.java — inside main
ObjectJar jar = new ObjectJar();
jar.put("sugar");
String a = (String) jar.get();
System.out.println("A: " + a);

jar.put(42);                       // the salt: a number, by mistake
System.out.println("B: put 42 in the jar");

String c = (String) jar.get();     // take it out, the same way as before
System.out.println("C: " + c);

Line 1 builds an empty jar. Line 2 puts the text "sugar" in it. Line 3 takes it out again. get hands back an Object, and you can't store an Object in a String box. So you need 1.2's cast, (String), which says "trust me, this is a String". Line 4 prints it.

Line 6 is the salt. 42 is a plain int. Java quietly wraps it into an Integer object for you, the way 1.1 showed. An Integer is an Object, so put takes it without a murmur. Line 7 prints a message to prove we got past that line.

Lines 9 and 10 do exactly what lines 3 and 4 did. Take the item out, cast it to String, print it.

The question is where it goes wrong. Most people point at the obvious line. Commit to yours.

Predict before you run — Lab 1

Three lines, A, B and C. Which of them print? If the program stops, which line stops it — the one that puts the number in, or a different one?

what actually happens (verified, JDK 17)
$ java Lab1_ObjectJar
A: sugar
B: put 42 in the jar
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at Lab1_ObjectJar.main(Lab1_ObjectJar.java:32)

B printed. Putting a number in a sugar jar went through without a word. Line 29 of Lab1_ObjectJar.java is the mistake. That's line 6 of the sample above, and Java had no complaint about it at all.

The crash is on line 32 of Lab1_ObjectJar.java, line 9 above, where you took the number out. A ClassCastException is Java's stop for a cast that lied. You said "trust me, this is a String". Java checked, found an Integer, and stopped the program. Read the message the way 0.4 taught you: Integer cannot be cast to class String. The part in brackets after it is detail about where Java keeps those two classes. You can skip it.

If you pointed at the jar.put(42) line, that was the sensible guess. It's the line that's wrong. But nothing on that line could know it. The jar takes anything, by design.

Here, the two lines are three apart. In a real program, put and get are in different files, written weeks apart. The crash tells you where the salt was found. It tells you nothing about who poured it.

Worth remembering

Section 2A blank on the label

We want a jar that knows what it's for. But we don't want a SugarJar, a CoinJar and a ButtonJar. They'd be the same apart from one word.

Think of it like this

A shop sells printed jar labels that say JAR OF ________. Same label for everyone, with one blank in it. You write SUGAR in the blank when you start using the jar. From then on, the label tells everybody what belongs inside. One design, and you choose the word each time.

Java lets you put a blank like that in a class. Here's src/Jar.java.

src/Jar.java
public class Jar<T> {

    private T item;

    public void put(T item) {
        this.item = item;
    }

    public T get() {
        return item;
    }
}

It's the same jar as Lab 1, with one change. Every Object became T, and <T> appeared after the class name.

Line 1, public class Jar<T>, is the label with the blank. <T> says: "this class has a blank in it, and inside the class I'll call that blank T". T isn't a class anyone wrote. It's a stand-in for whatever kind you choose later. That stand-in is a type parameter. A class with one is a generic class.

Line 3, private T item;, is the field. It holds a T, whatever T turns out to be. Lines 5 to 7 are put, which only accepts a T. Lines 9 to 11 are get, which hands back a T.

Why T? It's short for "type", and it's tradition. A single capital letter tells every reader "this is a blank, not a real class".

Why a jar and not a box

Textbooks usually call this class Box. In this course a box already means one variable's storage. Two meanings for one word is how readers get lost, so ours is a jar.

You've seen this split before

Chapter 0.4 drew a line between two words. A parameter is the name in the brackets when you write a method, like number in doubled(int number). An argument is the value you hand over when you use it, like the 21 in doubled(21).

Generics are the same split, one level up. You hand over a kind instead of a value. T in class Jar<T> is the type parameter. When you write Jar<String>, the String is the type argument. It fills the blank.

src/Lab2_GenericJar.java — inside main, first half
Jar<String> jar = new Jar<>();
jar.put("sugar");
String d = jar.get();              // no cast
System.out.println("D: " + d.toUpperCase());

// jar.put(42);                    // the salt again. Take the // off this line.

Line 1 makes a box called jar. Its label is Jar<String>, a jar with the blank filled in as String. On the right, new Jar<>() builds the jar. The empty <> means "same as the left", as 1.2 said. Its name is the diamond, after its shape.

Line 2 puts "sugar" in. The compiler checks: is "sugar" a String? Yes. Line 3 takes it out, and look what's missing. There's no cast. get hands back a T, and for this jar T is String, so the compiler already knows. Line 4 prints the sugar in capitals, to prove it really is a String with all of String's methods.

Line 6, // jar.put(42);, is switched off with //. It's Lab 1's salt, word for word.

Predict before you compile — the salt again

Take the // off the jar.put(42); line, save, and compile. In Lab 1 the number went in without a word. What happens now — and if there's an error, which line does it name?

what actually happens (verified, JDK 17)
$ javac Lab2_GenericJar.java
Lab2_GenericJar.java:17: error: incompatible types: int cannot be converted to String
        jar.put(42);                    // the salt again. Take the // off this line.
                ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

The program doesn't compile. And the error names line 17 of Lab2_GenericJar.java. That's jar.put(42);, the line where the salt goes in.

Read it word by word. incompatible types means "these two kinds don't fit together". int cannot be converted to String names both of them. You handed over an int. This jar's put takes a T, and T is String. The ^ points at the exact thing you handed over, the 42. The Note under it means javac shortened its own message. The short version is the useful one.

That's the whole point of the brackets. Lab 1's mistake was found on Saturday, at the take-out line, while the program ran. This one is found on Monday, at the put-in line, before the program can run at all. Put the // back before you go on.

Now the angle brackets you've been copying since 1.2 make sense. Somewhere inside Java, HashSet is written as class HashSet<E>, with a blank called E, for "element". When you typed HashSet<Money>, you filled the blank with Money. That's why 1.2 could say a String would be refused. It's this exact error.

Two blanks

A class can have more than one blank. src/Pair.java holds two things that travel together, each of a kind you choose.

src/Pair.java
public class Pair<A, B> {

    private final A first;
    private final B second;

    public Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }

    public A first() {
        return first;
    }

    public B second() {
        return second;
    }
}

Line 1 has two type parameters, A and B, separated by a comma. Lines 3 and 4 are two private final fields, the 1.2 way. One holds an A, the other a B. Lines 6 to 9 are the constructor, which fills both once. Lines 11 to 17 are two getters, each handing back its own kind.

src/Lab2_GenericJar.java — inside main, second half
Pair<String, Integer> ada = new Pair<>("Ada", 1815);
String name = ada.first();
int born = ada.second();
System.out.println("E: " + name + " was born in " + born);
System.out.println("F: " + (born + 200));

Line 1 fills both blanks: the first thing is a String, the second an Integer. Then it builds a pair from "Ada" and 1815. Line 2 takes out the first, with no cast. Line 3 takes out the second. second() hands back an Integer object, and Java unwraps it into a plain int for you. That's 1.1's wrapping trick, run backwards.

Line 4 prints both. Line 5 does a sum with born, to prove it's a real number.

what actually happens (verified, JDK 17)
$ java Lab2_GenericJar
D: SUGAR
E: Ada was born in 1815
F: 2015

No casts anywhere in main.

In Jar<String> jar = new Jar<>();, and the class public class Jar<T>, which is the type argument?

Worth remembering

Section 3Leave the brackets off

1.2 said one more thing about the brackets. Leave them out, and "nothing stops a String getting in". Time to run that sentence.

Every generic class can still be used with no brackets at all. Write Jar on its own and Java treats it like Lab 1's ObjectJar. A generic class used without its brackets is a raw type. It's there so that code written before 2004 still compiles.

src/Lab3_RawJar.java — inside main
Jar<String> sugar = new Jar<>();
Jar raw = sugar;                   // no brackets: the label is gone

raw.put(42);                       // the salt, through the side door
System.out.println("G: put 42 in the sugar jar");

String s = sugar.get();
System.out.println("H: " + s);

Line 1 makes a proper sugar jar, Jar<String>. Line 2 makes a second box, raw, labelled plain Jar, and copies the same ticket into it. There's still only one jar on the heap. 1.1's Lab 1 did exactly this with a dog.

Line 4, raw.put(42);, pours the salt in through raw. A raw Jar has no blank filled in, so put takes any object. Line 5 prints a message.

Line 7, String s = sugar.get();, takes the item out through sugar, the properly labelled box. No cast, as in Lab 2. Line 8 would print it.

Two bets here. Does javac let this file compile? And if it runs, which line stops it? Look hard at line 7 before you answer. It has no cast in it.

Predict before you run — Lab 3

Does Lab 3 compile? Does G print? Does H? If the program stops, which line number does the stack trace name?

what actually happens (verified, JDK 17)
$ javac Lab3_RawJar.java
Note: Lab3_RawJar.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$ java Lab3_RawJar
G: put 42 in the sugar jar
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at Lab3_RawJar.main(Lab3_RawJar.java:18)

It compiled, with a Note, and not an error. Then it ran, put a number in the sugar jar, and printed G.

Then it crashed on line 18 of Lab3_RawJar.java, which is String s = sugar.get();, line 7 above. Wait, what? That's a ClassCastException, a cast that lied. And there is no cast on that line. You didn't write one. Look again if you like.

Hold that thought. The next section finds the cast, and it's in a place you'd never think to look.

First, that Note. A warning is the compiler saying "this compiles, but I think it's wrong". The Note says this file has one, and offers more detail. Take it up on the offer.

what actually happens (verified, JDK 17)
$ javac -Xlint:unchecked Lab3_RawJar.java
Lab3_RawJar.java:15: warning: [unchecked] unchecked call to put(T) as a member of the raw type Jar
        raw.put(42);                       // the salt, through the side door
               ^
  where T is a type-variable:
    T extends Object declared in class Jar
1 warning

-Xlint:unchecked asks javac to explain its unchecked warnings in full. It names line 15 of Lab3_RawJar.java, raw.put(42);, where the salt went in. unchecked call to put(T) as a member of the raw type Jar means "you called put on a jar with no label, so I couldn't check what you put in". The last two lines say T is a blank declared in Jar.

This warning and the Note are the same thing. By default javac prints the Note, a one-line summary. -Xlint:unchecked shows each warning behind it.

So the compiler knew. It pointed at the right line, the put-in line, and then compiled the file anyway, because raw types are legal. The warning was the only thing standing between you and Saturday.

Treat that Note as a bug report

Never write a raw type in new code. Fill every blank: Jar<String>, HashSet<Money>. If you ever see "uses unchecked or unsafe operations", recompile with -Xlint:unchecked and read it. It names the line where your salt goes in.

Worth remembering

Section 4What the compiler throws away

To find that missing cast, you need to know what a Jar<String> looks like while the program runs. Here's a guess most people make. Somewhere inside the running program there's a Jar<String> and a separate Jar<Integer>, each knowing its own kind. Lab 4 asks Java directly.

src/Lab4_Erased.java — inside main
Jar<String> sugar = new Jar<>();
Jar<Integer> coins = new Jar<>();
System.out.println("I: " + (sugar.getClass() == coins.getClass()));
System.out.println("J: " + sugar.getClass().getName());

ArrayList<String> words = new ArrayList<>();
ArrayList<Integer> numbers = new ArrayList<>();
System.out.println("K: " + (words.getClass() == numbers.getClass()));
System.out.println("L: " + words.getClass().getName());

One new method first. getClass() is another starter method, like equals in 1.2, so every object has it. Java makes one object per class that stands for that class, and getClass() hands it back. Chapter 1.6 opens it up. getName() asks it for the class's name.

Lines 1 and 2 make a jar of Strings and a jar of Integers. Line 3 asks whether they belong to the same class, using == on those class objects. One per class, so == is the right question here. Line 4 prints the class's name.

Lines 6 to 9 ask the same two questions of Java's own ArrayList. That's a list that grows as you add to it. Section 5 meets it properly.

ArrayList isn't loaded unless you ask for it, so the file starts with one extra line, above the class.

src/Lab4_Erased.java — the top of the file
import java.util.ArrayList;

Line 1 is 1.2's one-liner again. An import line fetches a ready-made class by its full name, the way 0.3 fetched Scanner. Leave it out and javac says it cannot find the symbol ArrayList.

Predict before you run — Lab 4

Four lines, I to L. Is a jar of Strings the same class as a jar of Integers? And when you ask the jar for its class's name, does the name include <String>?

what actually happens (verified, JDK 17)
$ java Lab4_Erased
I: true
J: Jar
K: true
L: java.util.ArrayList

The same class. A jar of Strings and a jar of numbers are the same class while the program runs. Its name is Jar, with no brackets. Java's own ArrayList is the same, so this isn't something odd about our little jar.

If you guessed false, that was reasonable. The compiler treated them as different kinds, as Lab 2's error proved. But that difference lives in your source file, and only there.

The shopping list, not the jar

Think of it like this

Suppose the labels aren't on the jars at all. They're on your shopping list. At the shop you check every item against the list. So the salt never makes it into the basket marked sugar. At home, everything goes into plain, identical, unlabelled jars, and the list goes in the bin. It did its job. So how do you know a jar holds sugar when you open it? You taste it first. Every time.

That's what javac does. It reads your angle brackets and checks every line against them. Then, when it writes the compiled .class file, it throws the brackets away. Inside Jar.class, the field is an Object and every T is an Object. One plain jar for every kind. Throwing the type arguments away after checking them is called type erasure.

And the taste? That's the missing cast. Wherever you take something out of a jar, javac writes a cast into the compiled file for you. You can see it. The JDK comes with a tool called javap, which prints what's inside a .class file. -c asks for the code itself, as a list of tiny instructions. Point it at Lab 2, where line 14 of Lab2_GenericJar.java says // no cast.

what actually happens (verified, JDK 17)
$ javap -c Lab2_GenericJar
      11: invokevirtual #12                 // Method Jar.put:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #16                 // Method Jar.get:()Ljava/lang/Object;
      18: checkcast     #20                 // class java/lang/String

You don't need to read most of this. Each line is one instruction. The number on the left is its position inside the compiled code, not a line of your file. The text after // is javap explaining it in something closer to English. It writes a class name in the compiled file's own spelling: Ljava/lang/Object; means Object.

The first invokevirtual is jar.put("sugar"). Look at what put takes: Object. Not String. The aload_1 under it fetches jar again, ready for the next call. The second invokevirtual is jar.get(), and it hands back an Object.

The checkcast is the one. checkcast ... class java/lang/String is a cast to String. Your source line said // no cast. javac wrote one anyway, because it threw T away and needs the taste to keep its promise.

So now Lab 3 makes sense. String s = sugar.get(); really says (String) sugar.get() in the compiled file. The salt went in through the raw box, where nothing checked. The taste found it, at the take-out line. That's the crash at line 18 of Lab3_RawJar.java.

What javac does with your jar, one line at a time

On javac's shopping list

sugar is a Jar<String> · T = String

What it writes into the .class file

sugar is a Jar

Step 1 — Jar<String> sugar = new Jar<>(); javac writes on its shopping list that, for sugar, T means String. The compiled file says only Jar. The shopping list never leaves the compiler.

On javac's shopping list

sugar.put("sugar") is "sugar" a String? yes
sugar.put(42) is 42 a String? no — stop

What it writes into the .class file

call put(Object)

Step 2 — every put is checked against the list. "sugar" is a String, so javac allows it. It writes a call to the only put the compiled Jar has, one that takes any Object. Lab 2's 42 isn't a String, so javac stops with incompatible types. That's the check the brackets bought, at compile time.

On javac's shopping list

sugar.get() gives back a T, so a String

What it writes into the .class file

call get(), which gives back an Object
then checkcast String

Step 3 — String s = sugar.get(); You wrote no cast, because the list says it's a String. But the compiled get hands back an Object. So javac adds a checkcast straight after the call. That's the taste, the checkcast in the javap printout.

On javac's shopping list

raw.put(42) raw has no T — can't check. Warn.

Heap, while Lab 3 runs

jar #1 a plain Jar · item → Integer 42

Step 4 — Lab 3's raw box. raw has no brackets, so the list has nothing to check raw.put(42) against. javac warns, and writes the same put(Object) call as step 2. At run time the Integer lands in the one plain jar. Then sugar.get() runs step 3's taste, finds salt, and stops the program on the take-out line.

The one-line version

The brackets are checked at compile time and then thrown away. While the program runs, there is one Jar, and it holds Objects. The only check left is the cast javac adds wherever you take something out.

Going deeper Why Java throws the brackets away at all

Other languages keep their type arguments while the program runs. C#, for one, has a real, separate class for a list of strings. Java chose erasure, on purpose, in 2004.

The reason was the millions of lines of Java already written, all using raw ArrayLists. With erasure, a new ArrayList<String> compiles into the same thing as an old raw one. So old and new code could hand lists to each other unchanged. Raw types are the other half of that bargain.

The cost is the rest of this chapter. Section 5 shows one, and Section 6 shows the trick that works around another.

Worth remembering

Section 5Arrays remember. Lists forget.

Time to bring back 1.3's shapes. Shape is an interface that promises one method, area(). Circle and Square both keep that promise. Copies of all three are in this chapter's src/, so the folder stands on its own.

Here's a question that sounds like it has an obvious answer. A Circle is a Shape. 1.3 built a whole chapter on that. So a row of Circles is a row of Shapes too. Right?

Arrays and lists give two different answers. Lab 5 asks the array.

src/Lab5_ArrayStore.java — inside main
Circle[] circles = new Circle[2];
Shape[] shapes = circles;                  // the same array, a wider label

shapes[0] = new Circle(1);
System.out.println("M: stored a Circle");

shapes[1] = new Square(2);                 // a Shape... into an array of Circles
System.out.println("N: stored a Square");

Line 1 builds an array with two slots for Circles. Line 2 copies its ticket into a second box, labelled Shape[]. Still one array on the heap, with two tickets for it. 1.3 called the box's type its label, and this one is wider.

Line 4, shapes[0] = new Circle(1);, stores a circle through the wider label. Line 5 says so. Line 7, shapes[1] = new Square(2);, stores a square through it. A Square is a Shape, so the label Shape[] is happy. But the array was built for Circles. Line 8 would say it worked.

The same move, with a list

Lab 6 makes the same move with a list. Two tools first, both from Java's toolbox.

List is an interface, in 1.3's sense: a promise. Anything that's a List can add an item to the end and get the item at a position. It can tell you its size. Inside Java it's written interface List<E>, with a blank for the kind of element. ArrayList is a class that keeps that promise. Inside it is an ordinary 0.5 array. When the array fills up, the ArrayList builds a bigger one and copies everything across, so you never have to.

src/Lab6_ListStore.java — the top of the file
import java.util.ArrayList;
import java.util.List;

Lines 1 and 2 fetch both, by their full names. That's 1.2's import one-liner again. java.util is the part of the toolbox where the lists, sets and maps live.

src/Lab6_ListStore.java — inside main
List<Circle> circles = new ArrayList<>();
circles.add(new Circle(1));
System.out.println("O: stored a Circle");

// List<Shape> shapes = circles;           // the same list, a wider label
// shapes.add(new Square(2));              // a Shape... into a list of Circles

Line 1 makes a box labelled List<Circle>, the promise, and builds an ArrayList to keep it. The diamond copies Circle in from the left. Lines 2 and 3 add a circle and say so. Lines 5 and 6 are switched off. They're Lab 5's lines 2 and 7, for a list: a wider label, then a square through it.

Predict before you run — Labs 5 and 6

Run Lab 5. Does it compile? Which of M and N print? Then take the // off both switched-off lines in Lab 6 and compile. Same move — same result?

what actually happens (verified, JDK 17)
$ java Lab5_ArrayStore
M: stored a Circle
Exception in thread "main" java.lang.ArrayStoreException: Square
	at Lab5_ArrayStore.main(Lab5_ArrayStore.java:17)
what actually happens (verified, JDK 17)
$ javac Lab6_ListStore.java
Lab6_ListStore.java:18: error: incompatible types: List<Circle> cannot be converted to List<Shape>
        List<Shape> shapes = circles;           // the same list, a wider label
                             ^
1 error

The array compiled, ran, and stopped on line 17 of Lab5_ArrayStore.java. That's shapes[1] = new Square(2);, the line that stored the square. An ArrayStoreException is Java's stop for "you tried to put the wrong kind into an array". Its message is the kind you tried: Square.

The list didn't compile at all. javac refused the wider label itself, on line 18 of Lab6_ListStore.java. A List<Circle> doesn't count as a List<Shape>, even though a Circle counts as a Shape.

If that feels backwards, you're in good company. It's one of the most argued-about rules in Java. The stepper shows why both answers are right.

Why the array can check at run time, and the list can't

Stack — main's plate

circles = → array #1

Heap

array #1 note: made for Circle[] · empty · empty

Step 1 — new Circle[2]. Here's the key fact. Like every object in 1.3, the array carries a hidden note: made for Circle[]. The note stays for the array's whole life, and the array checks every store against it.

Stack — main's plate

circles = → array #1
shapes = → array #1 (label: Shape[])

Heap

array #1 note: made for Circle[] · empty · empty

Step 2 — Shape[] shapes = circles; A second ticket for the same array, under a wider label. The label is on the box. The hidden note is in the array. They now disagree, and Java allows it.

Stack — main's plate

circles = → array #1
shapes = → array #1 (label: Shape[])

Heap

array #1 note: made for Circle[] · a circle · empty

Step 3 — shapes[0] = new Circle(1); Every time something is stored, the array reads its hidden note. Is this a Circle? Yes. In it goes.

Stack — main's plate

circles = → array #1
shapes = → array #1 (label: Shape[])

Heap

array #1 note: made for Circle[] · refuses a Square

Step 4 — shapes[1] = new Square(2); The label said yes. The array reads its hidden note and says no, and stops the program with ArrayStoreException. Caught on the line where the wrong thing went in, though only at run time.

Stack — main's plate

circles = → list #1
shapes refused by javac

Heap

list #1 a plain ArrayList · no note about Circle · a circle

Step 5 — now the list. Thanks to erasure, the ArrayList on the heap has a hidden note saying only made by ArrayList. Nothing in it says Circle. It holds Objects. If javac allowed the wider label, nothing at run time could refuse the square. It would sit in a list of circles until somebody took it out and asked for its radius. That's Lab 1 all over again. So javac refuses the label instead.

The rule you take away

Arrays remember what they hold, so they can check late, while the program runs. Generic lists forget, so they must check early, while it compiles. Early is better: a compile error can't reach a user.

Going deeper The proper words, and why arrays work this way

"A Circle[] counts as a Shape[]" has a name. Arrays are covariant: the array types follow the same family tree as the things inside them. Generic types are invariant: List<Circle> and List<Shape> aren't related at all, whatever their type arguments are.

Why are arrays covariant? Java had arrays from the start, in 1996, almost nine years before generics. Without covariance, one method couldn't sort an array of anything. Arrays.sort(Object[]) needs to accept a String[]. The array's hidden note and ArrayStoreException were the price, paid on every store into an array of objects.

A list of some kind of Shape

So here's the problem the rule leaves you with. You want one method that adds up the area of a list of shapes. Any list: of circles, of squares, of mixed shapes. src/TotalArea.java has it.

src/TotalArea.java — the method, and main
static double totalArea(List<? extends Shape> shapes) {
    double total = 0;
    for (Shape s : shapes) {
        total = total + s.area();
    }
    return total;
}

public static void main(String[] args) {
    List<Square> tiles = new ArrayList<>();
    tiles.add(new Square(2));
    tiles.add(new Square(3));
    System.out.println("total area: " + totalArea(tiles));
}

Line 1 is the new idea. List<? extends Shape> reads as: "a list of some kind of Shape. I won't say which." The ? is a wildcard, a blank that stays blank. extends Shape says what it may be filled with: Shape, or any kind of one. So a List<Square> fits, and so do a List<Circle> and a List<Shape>.

Line 2 starts a running total at 0. Lines 3 to 5 are 0.5's for-each loop. Whatever kind the list really holds, each item is some kind of Shape. So it fits a box labelled Shape. Then s.area() asks it for its area, 1.3's one call. Line 6 hands back the total.

Lines 9 to 14 are main. Line 10 makes a list of squares, and lines 11 and 12 add a 2-by-2 and a 3-by-3 tile. Line 13 asks for their total area, which is 4 plus 9.

what actually happens (verified, JDK 17)
$ java TotalArea
total area: 13.0

Now take the wildcard away, and use what Lab 6 taught you.

Predict before you compile — no wildcard

Change line 1 to say List<Shape> shapes instead, and compile. main still hands it a List<Square>. Does it compile? If not, which line does javac name?

what actually happens (verified, JDK 17)
$ javac TotalArea.java
TotalArea.java:24: error: incompatible types: List<Square> cannot be converted to List<Shape>
        System.out.println("total area: " + totalArea(tiles));
                                                      ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

It doesn't compile, and the error is at the call, line 24 of TotalArea.java. It's Lab 6's rule: a List<Square> isn't a List<Shape>. The Note is the same one as in Lab 2.

If you said yes, you were thinking like an array. Very natural. But then nobody could hand the method a list of squares.

Why is the wildcard safe, when Lab 6's wider label wasn't? Look at what totalArea does to the list. It only reads. Take something out of "a list of some kind of Shape", and you always get a Shape. Whichever kind it really is.

Putting things in is a different story. Lab 5's danger was a square going into a list of circles. For a list of some unknown kind, javac can't know what's safe to add. So it refuses every add. Reading only, and the danger is gone.

And 1.3's FragileParent had a wildcard too. Its addAll took a collection whose brackets said ? extends String. Now you can read that out loud: a collection of some kind of String. addAll only reads from it, so it fits.

A method is declared static void printAll(List<Shape> shapes). You have a List<Circle> called wheels. What happens with printAll(wheels), and what's the fix?

Worth remembering

Section 6Asking for exactly the kind you want

One more problem, and it's the one real libraries hit every day. src/Shelf.java holds a row of shapes, of mixed kinds. Here's the plain part of it.

src/Shelf.java — the list and the plain getter
private final List<Shape> shapes = new ArrayList<>();

public void add(Shape shape) {
    shapes.add(shape);
}

public Shape get(int index) {
    return shapes.get(index);
}

Line 1 is the field: a list of any kind of Shape, built empty, right there in the field. 1.2's static int made = 0; filled a field the same way. Lines 3 to 5 add a shape to the end. Lines 7 to 9 hand back the shape at a position. The file imports List and ArrayList at the top, like Lab 6.

Now say you put a circle on the shelf at position 0, and you want its radius. shelf.get(0) hands back a Shape. And 1.3's Lab 2 showed what a Shape label lets you ask: area(), and nothing else. So you're back to a cast.

reading a radius, with a cast
double r = ((Circle) shelf.get(0)).radius();

Read it inside out. shelf.get(0) gives back a Shape. (Circle) casts it, "trust me". The outer brackets let you call .radius() on the result. It's Lab 1 with extra brackets. If position 0 ever holds a square, the cast lies and the program stops.

So let's try to do better. And let's try the obvious thing first, because nearly everyone does.

Attempt one: let the caller choose T

A method can have its own blank, separate from any class. Put <T> just before the return type, and that method has a type parameter of its own. That's a generic method. Here's one, in src/SloppyGet.java.

src/SloppyGet.java — the handy getter
static <T> T sloppyGet(Shelf shelf, int index) {
    Shape found = shelf.get(index);
    T value = (T) found;                        // line 11 of this file: a cast to T
    System.out.println("sloppyGet returned without complaint");
    return value;
}

Line 1 reads: a method with a blank called T, which hands back a T. Who fills the blank? You don't write it anywhere. Java works it out from where you put the answer. Store the result in a Circle box, and T is Circle for that call.

Line 2 fetches the shape with the plain getter. Line 3 casts it to T. Line 4 prints a message, so you can see the method finish. Line 5 hands it back.

src/SloppyGet.java — inside main
Shelf shelf = new Shelf();
shelf.add(new Circle(1));
shelf.add(new Square(2));

Circle round = sloppyGet(shelf, 0);
System.out.println("radius: " + round.radius());

Circle wrong = sloppyGet(shelf, 1);         // line 24 of this file: a Square, asked for as a Circle
System.out.println("never printed: " + wrong.radius());

Lines 1 to 3 build a shelf: a circle at position 0, a square at position 1. Line 5, Circle round = sloppyGet(shelf, 0);, asks for position 0 as a Circle. It really is one, so that's fine. Line 6 prints its radius.

Line 8, Circle wrong = sloppyGet(shelf, 1);, asks for the square as a Circle. That's the mistake. The method has a cast to T, on line 11 of SloppyGet.java. So where does it stop?

Predict before you run — the sloppy getter

Compile and run SloppyGet. Does javac say anything? How many times does sloppyGet returned without complaint print? And which line of SloppyGet.java does the trace name: line 11, the cast to T, or line 24?

what actually happens (verified, JDK 17)
$ javac SloppyGet.java
Note: SloppyGet.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$ java SloppyGet
sloppyGet returned without complaint
radius: 1.0
sloppyGet returned without complaint
Exception in thread "main" java.lang.ClassCastException: class Square cannot be cast to class Circle (Square and Circle are in unnamed module of loader 'app')
	at SloppyGet.main(SloppyGet.java:24)

sloppyGet finished both times, including the wrong one. Its cast to T let a square through as a "Circle". Then the program stopped on line 24 of SloppyGet.java, back in main. There's no line from sloppyGet in the trace at all.

You've seen why. T was erased, so inside the compiled method, (T) is a cast to Object. It checks nothing. The real check is the hidden cast javac put at the caller, on line 24 of the file. That's Section 4's taste, arriving late again. And javac warned you with that Note, the same Note as Lab 3.

Attempt two: check inside the method

Fine, you think. Make the method check before it hands anything back. 1.2 taught you instanceof. src/WhyTheToken.java has that version, switched off on one line.

src/WhyTheToken.java — the obvious version, switched off
// static <T extends Shape> boolean isA(Shape shape) { return shape instanceof T; }

It asks, while the program runs: is this shape a T? Ignore the extends Shape for one more minute. Take the // off and compile.

Predict before you compile — instanceof T

Does shape instanceof T compile? If not, can you say why, using what Section 4 showed you?

what actually happens (verified, JDK 17)
$ javac WhyTheToken.java
WhyTheToken.java:11: error: Shape cannot be safely cast to T
    static <T extends Shape> boolean isA(Shape shape) { return shape instanceof T; }
                                                               ^
  where T is a type-variable:
    T extends Shape declared in method <T>isA(Shape)
1 error

It won't compile. instanceof is a question asked while the program runs. By then, T has been thrown away. There's nothing left to ask about. So javac refuses, and says it can't check this safely.

If you thought it would work, that's the natural thought, and it's exactly why this section exists. Put the // back.

The fix: hand over the class itself

The method can't know T while it runs. So the caller must hand over something that does exist while it runs. Section 4 met it: Java makes one object per class that stands for that class. You can name that object in your code. Write a class name, then .class.

Circle.class is that object for Circle. It's called a class literal. Its own type is Class<Circle>: the class Class, with its blank filled in. An object handed over so a method knows a kind at run time is called a type token.

The real getter

Now the shelf's second get reads easily. Here it is.

src/Shelf.java — the getter with a token
public <T extends Shape> T get(int index, Class<T> type) {
    Shape shape = shapes.get(index);
    if (type.isInstance(shape) == false) {
        throw new ClassCastException("shape " + index + " is a "
                + shape.getClass().getSimpleName() + ", not a " + type.getSimpleName());
    }
    return type.cast(shape);
}

Line 1 has the last new idea: <T extends Shape>. That's a type parameter with a bound. extends Shape limits the blank: T must be Shape or a kind of one. Notice the word is extends even though Shape is an interface. In angle brackets, extends covers both. Then Class<T> type ties the token to the return type. Hand over Circle.class, and T is Circle, so you get a Circle back.

Line 2 fetches the shape at that position. Line 3 asks the token isInstance(shape). That's instanceof, asked of a class object you're holding, so it works while the program runs. Lines 4 and 5 deal with the wrong kind. They stop with a ClassCastException of their own, with a message in plain words. getClass().getSimpleName() asks an object for its class's short name, like Square. 1.6 opens that up. type.getSimpleName() does the same for the token.

Line 7 hands back type.cast(shape). That's a cast performed by the class object, so it really checks, unlike (T). Line 3 already made sure it will pass.

src/ShelfGet.java — inside main
Shelf shelf = new Shelf();
shelf.add(new Circle(1));
shelf.add(new Square(2));

Circle round = shelf.get(0, Circle.class);
System.out.println("radius: " + round.radius());

// String text = shelf.get(0, String.class);   // take the // off and compile

Circle wrong = shelf.get(1, Circle.class);
System.out.println("never printed: " + wrong.radius());

Lines 1 to 3 build the same shelf as SloppyGet. Line 5, Circle round = shelf.get(0, Circle.class);, asks for a circle and hands over the token. No cast anywhere. Line 6 prints its radius. Line 8 is switched off. The String.class step in Your turn, at the end, has a job for it. Line 10, Circle wrong = shelf.get(1, Circle.class);, makes SloppyGet's mistake, with the token.

Predict before you run — the real getter

Run ShelfGet. Which method is on the top at line this time? And what does the message say?

what actually happens (verified, JDK 17)
$ java ShelfGet
radius: 1.0
Exception in thread "main" java.lang.ClassCastException: shape 1 is a Square, not a Circle
	at Shelf.get(Shelf.java:28)
	at ShelfGet.main(ShelfGet.java:21)

Compare it with SloppyGet. The message is about your shelf: the position, the kind it found, the kind you wanted. Not class Square cannot be cast to class Circle (... loader 'app').

And the top plate is Shelf.get, line 28 of Shelf.java. That's the throw on lines 4 and 5 of the code above. The mistake was caught inside the getter, before a wrong shape could be handed back. The line under it says who asked: your main, at line 21 of ShelfGet.java. That's 0.4's way of reading a trace, and it walks you straight to your mistake.

Going deeper Why type.cast checks and (T) doesn't

After erasure, T becomes its bound, or Object if it has none. So SloppyGet's (T) became a cast to Object, which can never fail. With <T extends Shape> it would be a cast to Shape. That catches a non-shape, but never "a square, not a circle".

type.cast(shape) is a method call on a real object, the one that stands for Circle. Inside, it calls isInstance and throws if the answer is no. That object is the piece of T that survives erasure, because you handed it over yourself.

Worth remembering
Your turn

Everything is in this chapter's src/ folder. Compile it all once with javac *.java. You'll see the Note about unchecked operations. That's Lab 3 and SloppyGet, on purpose. 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.

Section 7Check yourself

A program crashes with a ClassCastException on String s = jar.get();, a line with no cast written on it. What's the most likely story?

Why does Shelf.get take a Class<T> type parameter, as in shelf.get(0, Circle.class), instead of working out the kind from T alone?

Judgment call. A teammate's code compiles with "Note: uses unchecked or unsafe operations". Every test passes. They want to ignore it. What do you say?

Section 8What 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 Lab 3, which line was the mistake, and which line crashed? Why were they different?
  3. Explain erasure to a friend using the shopping list and the taste. Then say where javap showed you the taste.
  4. Why does an array catch Lab 5's mistake while running, when a list has to catch it while compiling?
  5. Say out loud what <T extends Shape> T get(int index, Class<T> type) promises, one piece at a time.
Next up

Chapter 1.5 is about the crashes you've been reading all chapter. A ClassCastException is an object, and 1.5 shows how a program can catch one instead of stopping. Then in 1.7 you'll use this chapter to build the course's big project, a JSON library. Its second stage swaps plain arrays for real lists and maps. And it gets getters that take a type token, like Shelf's.

Nice. Chapter marked complete.