skip to content
everything.generics.whyGenerics

why generics exist

life before generics was ClassCastException at 3am.

chapter
chapter 05.01
reading time
9 min read

before java 5, every collection held Object. you cast on the way out, and a wrong element blew up at the read, far away from the line that put it there. generics let you write List<String>, so the compiler checks every add and writes every cast for you.

same bug, found at compile time instead of at 3am. that's the whole reason they exist.

the picture

you know how a ClassCastException stack trace always points at a line that looks perfectly fine? that's because the line that crashed isn't the line that's wrong. something else, somewhere else, put the wrong thing in the box. the crash is just where someone finally opened it.

think of a warehouse with unlabelled crates (a raw List holds Object). anyone can put anything in any crate, and you only find out a crate of shoes is in the fruit aisle when somebody opens it to make a smoothie. generics are the label on the crate, checked at the loading dock (javac). the wrong delivery gets turned away at the door, with the driver still standing there.

names is a
Main.java
  1. 1static List loadNames() { // a 2003-style API
  2. 2 List names = new ArrayList();
  3. 3 names.add("ada");
  4. 4 names.add(42);← the bug is here
  5. 5 return names;
  6. 6}
  7. 7
  8. 8void main() {
  9. 9 List names = loadNames();
  10. 10 for (Object o : names) {
  11. 11 String s = (String) o;← it explodes here
  12. 12 IO.println(s.toUpperCase());
  13. 13 }
  14. 14}
$javac Main.java
Note: Main.java uses unchecked or unsafe
operations.
Note: Recompile with -Xlint:unchecked for
details.
compiled. (-Xlint:all shows 6 warnings)
$java Main
ADA
Exception in thread "main"
java.lang.ClassCastException: class
java.lang.Integer cannot be cast to class
java.lang.String (...)
at Main.main(Main.java:11)

it compiles (two notes, easy to ignore), it runs, it prints ADA, and then it dies on line 11. the stack trace points at the cast, seven lines and one method away from the real bug. in a real codebase that's a different file, and maybe a different team.

flip the list between raw and List<String>. same program, same bug on line 4. watch where it gets caught: the real bug versus where java complains, and whether that happens before or after the program runs. all output is real jdk 25.

that distance is the whole problem. with a raw list, the error shows up where the list is read. with a generic one, it shows up where the list is written, which is where the bug actually is.

life before java 5

java 1.0 shipped in 1996 with Vector and Hashtable, and java 1.2 added the collections framework. all of it was built on one idea: everything is an Object, so a collection of Object can hold anything. true, and kind of useless, because you never want "anything". you want strings, or orders, or a map from names to lists of scores.

don't2003: casts all the way down
java
Map scores = new HashMap();
List list = new ArrayList();
list.add(Integer.valueOf(91));
scores.put("ada", list);
 
int first = ((Integer) ((List) scores
    .get("ada")).get(0)).intValue();
do2004 onwards: the types do the work
java
Map<String, List<Integer>> scores =
    new HashMap<>();
scores.computeIfAbsent("ada",
    k -> new ArrayList<>()).add(91);
 
int first = scores.get("ada").get(0);

the left side has two casts and an intValue(), and nothing on that page tells you a scores value is a List of Integers. that knowledge lived in comments, in variable names, and in people's heads. generics moved it into the type, where javac can check it.

Main.java
void main() {
    Map<String, List<Integer>> scores = new HashMap<>();
    scores.computeIfAbsent("ada", k -> new ArrayList<>()).add(91);
    scores.computeIfAbsent("ada", k -> new ArrayList<>()).add(78);
 
    int first = scores.get("ada").get(0);          // no casts, two levels deep
    for (int s : scores.get("ada")) IO.println(s);
    IO.println("first: " + first);
}
output
91
78
first: 91

hover a stage.

how java got here. generics, autoboxing and the for-each loop all arrived together in java 5, which is why the right-hand code above looks so different.

what the compiler does for you

two jobs, and that's all generics are:

  1. check every write. names.add(42) on a List<String> doesn't compile.
  2. write every cast. String s = names.get(0) compiles to a call that returns Object, followed by a cast to String that javac inserted. you never type it, and as long as nobody goes around job 1, it can never fail.

notice what's missing: the list itself didn't change. an ArrayList<String> and an ArrayList<Integer> are the same class at run time, both backed by a plain Object[]. the type argument lives in the compiler's head, not in the object.

stack
main
names
raw
heap
ArrayList
elementData
size2
Object[]
0
1
String
"ada"
Integernot a String
42
one list, two variables pointing at it: names is declared List<String>, raw is a raw List. the object has no idea which one you used. its array is an Object[], and the Integer got in through the raw door.

that's why you can sneak a wrong element in: the checks are a compile-time contract, and raw types let you step outside it. and when nobody needs the element to be a String, nothing even notices.

predict the output
java
List<String> names = new ArrayList<>();
List raw = names;
raw.add(42);                       // a warning, not an error
IO.println(names.size());
IO.println(names.get(0));
String s = names.get(0);

why not something else?

why not keep Object and be careful? people did, for eight years. "be careful" doesn't scale past one person and one file. the whole point of a type system is that the computer is careful for you.

why not arrays? String[] is already typed. it is, and arrays do check every store, but at run time, with an ArrayStoreException. that's earlier than a cast failing on the way out, still later than a compile error. and arrays have their own holes (arrays, varargs and other limits).

why not a class per element type, StringList, OrderList? that's roughly what C++ templates do: the compiler stamps out a separate copy of the code for every type argument. java chose the opposite. one ArrayList class, shared by every type argument, with the types checked by javac and then thrown away. it meant the JVM didn't have to change at all, and old class files kept working next to new ones. the price of that choice is type erasure, and most of the weird corners in this part come from it.

raw types still compile

for compatibility, every generic type still has a raw form: List with no angle brackets. it exists so code from 2003 compiles today. it also switches the checks off, and javac only mutters about it. by default you get two Note: lines. ask for everything and you see what it's actually worried about (first line of each warning, from javac 25, for the raw program in the figure):

terminal
$ javac -Xlint:all Main.java
Main.java:1: warning: [rawtypes] found raw type: List
Main.java:2: warning: [rawtypes] found raw type: List
Main.java:2: warning: [rawtypes] found raw type: ArrayList
Main.java:3: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
Main.java:4: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
Main.java:9: warning: [rawtypes] found raw type: List
6 warnings
keylines
  • before java 5, collections held Object and every read was a cast you wrote by hand.
  • a raw collection fails where it's read. a generic one fails where it's written, which is where the bug is.
  • generics do two jobs: check every write, insert every cast.
  • the object doesn't know its type argument. the compiler does.
  • raw types still compile for compatibility. treat their warnings as bugs.