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.
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
- Write your own class with a blank in its label, like
Jar<T>, and say whatTstands for. - Point at the line where a wrong-kind mistake happens, and the line where it finally crashes.
- Read
<T extends Shape>andList<? extends Shape>out loud, in plain words. - Say why arrays and lists disagree, and show what the compiler throws away by running it.
- Write a getter like
shelf.get(0, Circle.class)that really checks, and explain why the.classis there.
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.
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.
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.
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?
$ 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.
- A field of type
Objectcan hold anything. So a jar built onObjectcan't tell you what's in it. - Taking something out of it needs a cast. A cast that lies stops the program with a
ClassCastException. - The crash happens where the wrong thing is taken out, not where it was put in. Those can be far apart.
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.
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.
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".
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.
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.
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?
$ 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 errorThe 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.
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.
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.
$ java Lab2_GenericJar
D: SUGAR
E: Ada was born in 1815
F: 2015No casts anywhere in main.
In Jar<String> jar = new Jar<>();, and the class public class Jar<T>, which is the type argument?
class Jar<T>has a blank in its label.Tis a type parameter, a stand-in for a kind chosen later.Jar<String>fills the blank.Stringis the type argument.<>on the right means "same as the left".- With the blank filled, the compiler checks every
putand needs no cast onget. - The wrong kind is refused on the line where it goes in, before the program runs. That's what the brackets are for.
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.
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.
Does Lab 3 compile? Does G print? Does H? If the program stops, which line number does the stack trace name?
$ 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.
$ 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.
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.
- A generic class written without its brackets is a raw type. It checks nothing, like
ObjectJar. - Raw types still compile, so old code keeps working. You get a Note, not an error.
javac -Xlint:uncheckedturns the Note into a warning that names the exact line.- Lab 3 crashed with a
ClassCastExceptionon a line with no cast in it. Section 4 explains.
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.
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.
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.
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>?
$ java Lab4_Erased
I: true
J: Jar
K: true
L: java.util.ArrayListThe 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
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.
$ 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/StringYou 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.
On javac's shopping list
What it writes into the .class file
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
What it writes into the .class file
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
What it writes into the .class file
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
Heap, while Lab 3 runs
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 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.
- Type erasure:
javacchecks your angle brackets, then throws them away.Jar<String>andJar<Integer>are one class,Jar, at run time. - Inside the compiled
Jar, everyTisObject. - Wherever you take something out,
javacadds a cast for you.javap -cshows it ascheckcast. - So a wrong-kind item that sneaks past the compiler crashes at the take-out line, with a cast you never wrote.
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.
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.
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.
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 CirclesLine 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.
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?
$ java Lab5_ArrayStore
M: stored a Circle
Exception in thread "main" java.lang.ArrayStoreException: Square
at Lab5_ArrayStore.main(Lab5_ArrayStore.java: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 errorThe 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.
Stack — main's plate
Heap
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
Heap
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
Heap
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
Heap
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
Heap
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.
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.
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.
$ java TotalArea
total area: 13.0Now take the wildcard away, and use what Lab 6 taught you.
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?
$ 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 errorIt 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?
List<E>is a promise:add,get,size.ArrayListkeeps it, with an array inside that grows.- A
Circle[]counts as aShape[]. AList<Circle>does not count as aList<Shape>. - Arrays remember their kind, so a wrong store is caught at run time:
ArrayStoreException. - Lists forget their kind (erasure), so
javacmust refuse the wider label up front. List<? extends Shape>means "a list of some kind ofShape". Any such list fits. You can read from it, but not add to it.
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.
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.
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.
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.
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?
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?
$ 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.
// 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.
Does shape instanceof T compile? If not, can you say why, using what Section 4 showed you?
$ 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 errorIt 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.
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.
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.
Run ShelfGet. Which method is on the top at line this time? And what does the message say?
$ 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.
<T> T method(...)is a generic method: its own blank, filled from where you put the answer.(T) xchecks nothing after erasure. The crash comes later, at the caller, like Lab 3.instanceof Twon't compile, becauseTdoesn't exist while the program runs.- So the caller hands over a type token, like
Circle.class.type.isInstanceandtype.castreally check. <T extends Shape>is a bound:Tmust be a kind ofShape.
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:
- Which predictions did you get wrong? For each one, what were you picturing instead?
- In Lab 3, which line was the mistake, and which line crashed? Why were they different?
- Explain erasure to a friend using the shopping list and the taste. Then say where
javapshowed you the taste. - Why does an array catch Lab 5's mistake while running, when a list has to catch it while compiling?
- Say out loud what
<T extends Shape> T get(int index, Class<T> type)promises, one piece at a time.
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.