Topic 8.1
Why Generics Exist
In one line
Generics let a class or method work with many types while the compiler still checks which type you used. List<String> is a list the compiler knows holds only strings, so wrong elements become compile errors and the casts disappear.
Think of it like this
A school has labelled lunch boxes. A box labelled "sandwich" only ever gets sandwiches, so when you open it you know what you'll find. An unlabelled box could hold anything: you have to look inside and guess, and one day you bite into a crayon. Before generics, every Java collection was an unlabelled box. Generics put a label on the box that the compiler reads and enforces.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Generics
- The Java feature that lets classes and methods take a type as a parameter, like
List<String>, so the compiler can check element types. - Type parameter
- A placeholder name for a type, declared in angle brackets, like the
Einclass ArrayList<E>. - Type argument
- The real type you put in the brackets when you use a generic type, like
StringinList<String>. - Cast
- Telling Java to treat a value as a more specific type, like
(String) obj. If the object isn't really that type, the program throwsClassCastException. - ClassCastException
- The runtime error Java throws when a cast is wrong: the object is not an instance of the type you cast it to.
- Raw type
- A generic type used without type arguments, like plain
List. It exists for old code and turns off the compiler's type checks. - Autoboxing
- Java automatically wrapping a primitive like
5into its object formInteger.valueOf(5), and unwrapping it back (unboxing). - Unchecked warning
- A compiler warning that says "I can't prove this is type-safe". Code with no unchecked warnings can't get a hidden
ClassCastExceptionfrom generics.
Step by step
01Life before Java 5: everything is an Object
In Java 1.4, List had methods add(Object o) and Object get(int i). Any object fits in an Object parameter, so the compiler accepts every add.
Reading is the painful part: get returns Object, and you can't call toUpperCase() on an Object, so you must cast. The cast is a promise you make to the compiler ("trust me, it's a String"), and nobody checks it until the program runs.
List names = new ArrayList(); // raw type: elements are Object
names.add("Asha");
names.add(42); // compiles: 42 is boxed to Integer, an Object
String first = (String) names.get(0); // fine
String second = (String) names.get(1); // compiles, then ClassCastException at run time02Why a runtime failure is so expensive
The bug is the line that added 42. The crash is the line that reads it, which might be in another class, run hours later, on a production server. The stack trace points at the reader, not the writer, so you have to hunt for who put the wrong thing in.
A compile error, by contrast, points at the exact wrong line before the program ever runs. Generics move this whole class of bug from run time to compile time.
03Put a type argument on the box
List<String> names = new ArrayList<>(); says: this list holds Strings. The List interface is declared interface List<E>, and you've bound E to String for this variable.
Now the compiler checks every call against String. names.add(42) is rejected on the spot, and names.get(0) has type String, so no cast is needed.
04The cast is still there, but the compiler proved it
Generics are a compile-time feature. The ArrayList object at run time still stores Object references in an Object[] array (you'll see why in Topic 8.6, type erasure). When you write String s = names.get(0);, the compiler emits invokeinterface List.get followed by checkcast java/lang/String.
That checkcast can't fail, because every path that put something in the list was checked to be a String. You get the safety of a checked type plus the speed of the same bytecode Java 1.4 produced.
05Only reference types: List<Integer>, not List<int>
A type argument must be a class, interface, array or another type parameter. Primitives (int, double, boolean...) aren't allowed, because the generic code underneath works with Object references, and an int isn't an object.
Use the wrapper class instead: List<Integer>. Autoboxing converts 5 to Integer.valueOf(5) on the way in and unboxes on the way out. Each element is then a reference on the list's array pointing to an Integer object on the heap.
06The diamond: let the compiler fill in the type (Java 7)
In Java 5 and 6 you had to repeat yourself: Map<String, List<Integer>> m = new HashMap<String, List<Integer>>();. Java 7 added the diamond <>: new HashMap<>() asks the compiler to infer the type arguments from the variable you're assigning to.
new HashMap() without the diamond is different: it creates a raw type and gives an unchecked warning. The two characters <> matter.
Map<String, List<Integer>> scores = new HashMap<String, List<Integer>>(); // Java 5 style
Map<String, List<Integer>> scores2 = new HashMap<>(); // Java 7 diamond
var scores3 = new HashMap<String, List<Integer>>(); // Java 10 var: type on the right07Raw types: still legal, never wanted
Java 5 had to run millions of lines of existing code, so the generic List also had to accept the old raw List. The compiler allows raw types but warns with an unchecked warning. By default it only prints a short note; -Xlint:unchecked shows each one.
Treat these warnings as bugs in waiting. When you must silence one (for example after a cast you have reasoned about), use @SuppressWarnings("unchecked") on the smallest possible declaration and write a comment saying why it's safe.
Try it yourself
- 1
Uncomment the bad add
In the second example, uncomment
names.add(42);. Predict: does it compile? Which line does the error point at? Run it and compare with the message in the walkthrough. - 2
Find where the raw-list bug really is
In the first example, change
names.add(42)tonames.add(new StringBuilder("Meera")). Predict the third output line before running. Then change the declaration toList<String>and remove the cast and@SuppressWarnings: what does the compiler say now? - 3
Try a primitive type argument
In the box example, change
Box<Integer>toBox<int>and compile. Read therequired: referencemessage, then change it back.
Code & diagrams
The wrong element went in on line 10, but the program only finds out when it reads it back.
Expected output
0: ASHA
1: RAVI
2: ClassCastException, element type: IntegerExpected output
ASHA
RAVI
Asha's marks: [90, 85, 77]
total: 252One generic class, Box<T>, serves every type safely. You'll write classes like this in Topic 8.2.
Expected output
ObjectBox: tea
Box<String>: 4 letters
Box<Integer>: 42Raw types let an Integer into a List<String>. A checked list catches it at the faulty add, which is where you want the stack trace.
Expected output
plain list now holds: [99]
checkedList rejected the add
guarded list: []Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Add the wrong type to a typed list
With List<String> names = new ArrayList<>();, write names.add(42);.
Break #2
Use a primitive as a type argument
Write List<int> marks = new ArrayList<>();.
Break #3
Read from a raw list without a cast
Declare List names = new ArrayList(); and write String first = names.get(0);.
Myth vs fact
Myth
Generics make a List<String> store Strings differently at run time.
Fact
At run time an ArrayList<String> and an ArrayList<Integer> are the same class with the same Object[] inside. Generics are checked by the compiler and then erased (Topic 8.6).
Myth
Generics remove casts, so they make code faster.
Fact
The compiler still inserts a checkcast at each read. Speed is the same as the hand-written cast; what you gain is the guarantee that it can't fail.
Myth
Raw types are just a shorter way to write List<Object>.
Fact
List<Object> is fully checked: it accepts anything and you read Object back, honestly. A raw List switches checks off, so it can be assigned to and from List<String> with only a warning, which is how wrong elements sneak in.
Myth
You can put int into a generic list.
Fact
You can write list.add(5), but autoboxing turns it into an Integer object. The list never holds primitives.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Generics came from JSR 14, based on the GJ ("Generic Java") research compiler. The key design constraint was migration compatibility: generic library classes had to work with old, non-generic client code without recompiling it. That constraint is why Java chose erasure instead of the reified generics C# has.
- ▸
The guarantee is precise: if the whole program compiles with no unchecked warnings, every compiler-inserted
checkcastsucceeds. AClassCastExceptionon a line with no visible cast means somebody, somewhere, ignored an unchecked warning or used a raw type. This failure is called heap pollution. - ▸
Boxing has a real cost. An
inttakes 4 bytes; anIntegeron a 64-bit HotSpot JVM with compressed references is a 16-byte object plus a 4-byte reference.Integer.valueOfcaches -128 to 127, so small values share objects, which is also why==onIntegerworks by accident for small numbers and fails for big ones. - ▸
The JDK's
Collections.checkedList/Map/Setwrappers check every insert against aClassobject at run time. They exist exactly for code bases that still mix raw and generic code: wrap the collection, and the stack trace points at the polluting call.
Remember this
- 1
Before Java 5,
ArrayListstored elements asObject, the type every class extends. You could add aString, anIntegerand aScannerto the same list, and the compiler said nothing. Reading an element back gave you anObject, so you had to cast it:String s = (String) list.get(0);. If the element wasn't really aString, the cast failed at run time with aClassCastException, often in a different method, file or week from the line that added the wrong thing. - 2
A generic type has a type parameter, a placeholder for a type written in angle brackets.
ArrayListis declared asclass ArrayList<E>;Estands for "element type". When you writeArrayList<String>, you supply the type argumentString, and the compiler treatsadd(E e)asadd(String e)andE get(int i)asString get(int i). Adding anIntegeris now a compile error, andgetneeds no cast. - 3
The compiler still inserts the cast for you, behind the scenes. In the bytecode,
list.get(0)returnsObjectand is followed by acheckcast Stringinstruction. The difference is that the compiler has proved the cast will succeed, because it checked everyadd. That is the core promise of generics: if your code compiles without unchecked warnings, these hidden casts never fail. - 4
Type arguments must be reference types:
List<Integer>works,List<int>is a compile error (unexpected type). Java 5 added autoboxing at the same time, solist.add(5)quietly becomeslist.add(Integer.valueOf(5)). The price is that each element is a separateIntegerobject on the heap, reached through a reference, which costs memory and speed compared with anint[]. - 5
Old code without type arguments still compiles. A generic type used without
<...>, like plainList, is called a raw type, and it exists only so pre-2004 code kept working. The compiler warns (unchecked call to add(E) as a member of the raw type List) because it can no longer protect you. New code should never use raw types. - 6
Generics also make APIs self-documenting and enable reusable algorithms.
Map<String, List<Integer>>tells a reader exactly what's inside, and one method likeCollections.sortorCollections.maxworks for every element type without being rewritten. The rest of this phase shows how to write your own: generic classes (8.2), generic methods (8.3), bounds (8.4), wildcards (8.5), and the erasure rules underneath them all (8.6).
Explain it without notes
What problem did generics solve? Describe how the same bug behaves with and without them.
What is the difference between a type parameter and a type argument?
Why can't you write List<int>, and what happens when you add an int to a List<Integer>?
What is a raw type, why does Java still allow it, and why should you avoid it?
Practice
Write a program that stores three city names in a List<String>, then prints the total number of characters, without any casts.
Write a Map<String, Integer> word counter for the words {"tea", "chai", "tea", "coffee", "tea"}, using a TreeMap so the output order is fixed, and print the map.
Write a generic-free ObjectPair class holding two Objects, then a generic Pair<A, B>. Store ("Asha", 92) in each and print the name length and the mark plus 1, showing where casts are needed.
Trade-offs
- ↔
Generics give compile-time safety and clearer APIs at the cost of more complex signatures.
Map<String, List<Integer>>is longer thanMap, and bounded wildcard signatures (Topic 8.5) take practice to read. - ↔
Because type arguments must be objects, numeric collections pay for boxing: more memory, more garbage, and pointer chasing. Hot numeric code often uses
int[]or specialised libraries instead ofList<Integer>. - ↔
Java's erased generics kept old code working (a huge win in 2004), but they also bring limits you'll meet in Topic 8.6: no
new T(), noinstanceof List<String>, no generic arrays.
Done when you can
Done when you can explain the pre-generics
ClassCastExceptionproblem and why it was hard to debug.Done when you can name the type parameter and type argument in
List<String>.Done when you can explain why
List<int>fails and what autoboxing does instead.Done when you recognise a raw type and the unchecked warning it causes.
Done when you use the diamond
<>and know it's different from a rawnew ArrayList().