Command Palette

Search for a command to run...

PHASE 8Intermediate Java 5+ ~31 min· topic 1 of 6

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 E in class ArrayList<E>.
Type argument
The real type you put in the brackets when you use a generic type, like String in List<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 throws ClassCastException.
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 5 into its object form Integer.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 ClassCastException from 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.

Main.javawhole filejava
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 time

02Why 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.

Why a runtime failure is so expensivediagram
Rendering diagram…

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.

terminal
$ javac Main.java
── expected output ──
Main.java:7: error: incompatible types: int cannot be converted to String
names.add(42);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

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.

terminal
$ javac Main.java
javap -c Main | grep -A1 List.get
── expected output ──
19: invokeinterface #18, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
24: checkcast #22 // class java/lang/String

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.

terminal
$ javac Main.java
── expected output ──
Main.java:6: error: unexpected type
List<int> marks = new ArrayList<>();
^
required: reference
found: int
1 error
Only reference types: List<Integer>, not List<int>diagram
Rendering diagram…

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.

Main.javawhole filejava
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 right

07Raw 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.

terminal
$ javac -Xlint:unchecked Main.java
── expected output ──
Main.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
names.add("Asha");
^
where E is a type-variable:
E extends Object declared in interface List
1 warning

Try it yourself

  1. 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. 2

    Find where the raw-list bug really is

    In the first example, change names.add(42) to names.add(new StringBuilder("Meera")). Predict the third output line before running. Then change the declaration to List<String> and remove the cast and @SuppressWarnings: what does the compiler say now?

  3. 3

    Try a primitive type argument

    In the box example, change Box<Integer> to Box<int> and compile. Read the required: reference message, then change it back.

Code & diagrams

The pre-generics bug: a raw list and a bad cast New tab

The wrong element went in on line 10, but the program only finds out when it reads it back.

Sign in to run this example in your browser.

Expected output

0: ASHA
1: RAVI
2: ClassCastException, element type: Integer
The same program with generics: no casts, no surprises New tab
Sign in to run this example in your browser.

Expected output

ASHA
RAVI
Asha's marks: [90, 85, 77]
total: 252
A box for Object versus a generic box New tab

One generic class, Box<T>, serves every type safely. You'll write classes like this in Topic 8.2.

Sign in to run this example in your browser.

Expected output

ObjectBox: tea
Box<String>: 4 letters
Box<Integer>: 42
Turning a hidden runtime bug into an early one with Collections.checkedList New tab

Raw 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.

Sign in to run this example in your browser.

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);.

terminal
$ javac Main.java
── what you'll see ──
Main.java:7: error: incompatible types: int cannot be converted to String
names.add(42);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

Break #2

Use a primitive as a type argument

Write List<int> marks = new ArrayList<>();.

terminal
$ javac Main.java
── what you'll see ──
Main.java:6: error: unexpected type
List<int> marks = new ArrayList<>();
^
required: reference
found: int
1 error

Break #3

Read from a raw list without a cast

Declare List names = new ArrayList(); and write String first = names.get(0);.

terminal
$ javac Main.java
── what you'll see ──
Main.java:8: error: incompatible types: Object cannot be converted to String
String first = names.get(0);
^
Note: Main.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
1 error

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 checkcast succeeds. A ClassCastException on 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 int takes 4 bytes; an Integer on a 64-bit HotSpot JVM with compressed references is a 16-byte object plus a 4-byte reference. Integer.valueOf caches -128 to 127, so small values share objects, which is also why == on Integer works by accident for small numbers and fails for big ones.

  • ▸

    The JDK's Collections.checkedList/Map/Set wrappers check every insert against a Class object 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. 1

    Before Java 5, ArrayList stored elements as Object, the type every class extends. You could add a String, an Integer and a Scanner to the same list, and the compiler said nothing. Reading an element back gave you an Object, so you had to cast it: String s = (String) list.get(0);. If the element wasn't really a String, the cast failed at run time with a ClassCastException, often in a different method, file or week from the line that added the wrong thing.

  2. 2

    A generic type has a type parameter, a placeholder for a type written in angle brackets. ArrayList is declared as class ArrayList<E>; E stands for "element type". When you write ArrayList<String>, you supply the type argument String, and the compiler treats add(E e) as add(String e) and E get(int i) as String get(int i). Adding an Integer is now a compile error, and get needs no cast.

  3. 3

    The compiler still inserts the cast for you, behind the scenes. In the bytecode, list.get(0) returns Object and is followed by a checkcast String instruction. The difference is that the compiler has proved the cast will succeed, because it checked every add. That is the core promise of generics: if your code compiles without unchecked warnings, these hidden casts never fail.

  4. 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, so list.add(5) quietly becomes list.add(Integer.valueOf(5)). The price is that each element is a separate Integer object on the heap, reached through a reference, which costs memory and speed compared with an int[].

  5. 5

    Old code without type arguments still compiles. A generic type used without <...>, like plain List, 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. 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 like Collections.sort or Collections.max works 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

01

What problem did generics solve? Describe how the same bug behaves with and without them.

02

What is the difference between a type parameter and a type argument?

03

Why can't you write List<int>, and what happens when you add an int to a List<Integer>?

04

What is a raw type, why does Java still allow it, and why should you avoid it?

Practice

01

Write a program that stores three city names in a List<String>, then prints the total number of characters, without any casts.

02

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.

03

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 than Map, 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 of List<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(), no instanceof List<String>, no generic arrays.

Done when you can

  • Done when you can explain the pre-generics ClassCastException problem 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 raw new ArrayList().