What Are Generics in Java? Type Safety and Raw Types
Java generics let the compiler check what a collection holds, so wrong types fail at compile time with no casts. Vocabulary, the diamond, raw types, erasure.
- Course: Java study plan
- Module: Generics
- Kind: Lesson
- Reading time: 12 min
- Runtime: OpenJDK 18
What are generics in Java?
Generics in Java let a class, interface or method take type parameters, so List<String> is a list the compiler knows holds only strings. Adding the wrong type becomes a compile error on the line that is wrong, reading needs no cast, and one implementation serves every element type. Added in Java 5, generics are checked at compile time and erased at run time.
Lesson
Before Java 5, a List held Objects: you put a String in and got an Object out, cast it back, and found out at run time if you were wrong. Generics let you say List<String> — a list of strings — so the compiler checks what goes in and knows what comes out. This lesson is the problem generics solve, the vocabulary (type parameter, type argument, parameterised type, raw type), and the first look at the design decision — erasure — that explains every limitation you will meet later in the module.
The problem
List names = new ArrayList(); // pre-generics: a list of Object
names.add("Ada");
names.add(42); // nothing stops this
String first = (String) names.get(0); // cast required…
String second = (String) names.get(1); // …and this one throws ClassCastException at RUN time
Three costs: casts everywhere, no documentation of what the list holds, and errors that surface far from their cause — the 42 was added in one file and exploded in another.
The solution
List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42); // compile error: incompatible types: int cannot be converted to String
String first = names.get(0); // no cast: the compiler knows it is a String
List<String> is a parameterised type: the generic interface List<E> with the type argument String supplied for its type parameter E. Inside List, every E now means String, so add(E) accepts only strings and get(int) returns one. The error moves to compile time, to the line that is actually wrong.
Vocabulary
| Term | Example | Meaning |
|---|---|---|
| Generic type | List<E>, Map<K, V> | A type declared with type parameters |
| Type parameter | E, K, V, T | A placeholder in the declaration |
| Type argument | String in List<String> | The actual type supplied at use |
| Parameterised type | List<String> | A generic type with arguments |
| Raw type | List | A generic type used without arguments — legacy, unsafe |
| Generic method | static <T> T first(List<T> xs) | A method with its own type parameters |
| Bounded type | <T extends Comparable<T>> | A parameter restricted to subtypes of something |
| Wildcard | List<?>, List<? extends Number> | An unknown type argument, possibly bounded |
Conventions: single capital letters — T (type), E (element), K/V (key/value), R (result), N (number). Two-letter names are unusual; descriptive names are not idiomatic.
Type arguments must be reference types
List<int> is a compile error; List<Integer> with autoboxing is how primitives get in. This costs boxing (Module 2) and is the reason IntStream, int[] and primitive-specialised functional interfaces exist. Project Valhalla is working on lifting this; for now, wrappers.
The diamond
Map<String, List<Integer>> index = new HashMap<String, List<Integer>>(); // Java 5–6
Map<String, List<Integer>> index = new HashMap<>(); // Java 7+: infer from the left side
var index = new HashMap<String, List<Integer>>(); // Java 10+: infer the variable's type instead
<> tells the compiler to infer the constructor's type arguments from the target type. Combine it with var and the arguments must be on the right instead — never var x = new HashMap<>(), which infers HashMap<Object, Object>.
Raw types: why they still exist and why you avoid them
List without arguments is a raw type. It compiles for backwards compatibility with pre-2004 code, with warnings ("unchecked call"), and it turns off checking:
List<String> safe = new ArrayList<>();
List raw = safe; // allowed, warning
raw.add(42); // allowed, warning — pollutes the String list
String s = safe.get(0); // ClassCastException here, far from the raw add
Never write a raw type in new code. The one legitimate appearance is in class literals and instanceof (List.class, x instanceof List), because those cannot carry type arguments — the next section explains why.
Erasure, in one paragraph
Generics were added to a language with ten years of compiled libraries and a JVM that knew nothing about them. The design chosen was erasure: type arguments exist for the compiler only. After checking, List<String> compiles to plain List, T becomes Object (or its bound), and casts are inserted where the code reads elements. At run time there is one List class, no List<String>, and new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() is true. This kept old code running unchanged and is the reason for every "you cannot do that with generics" rule in lesson 5. Understand it now and the rules will feel inevitable rather than arbitrary.
What you gain, concretely
- Compile-time safety: wrong-type insertions are errors at the line that is wrong.
- No casts in reading code.
- Documentation:
Map<CustomerId, List<Order>>says what a structure holds;Mapsays nothing. - Reusable algorithms: one
sort, onemax, onefilterfor every element type, withoutObjectand casts. - APIs that describe themselves:
Function<String, Integer>tells you the whole contract.
Reading generic signatures
public static <T extends Comparable<? super T>> void sort(List<T> list) — read it left to right: a generic method with parameter T, where T must be comparable to itself or a supertype; takes a list of T; returns nothing. By the end of this module every part of that line will be obvious. Until then, the skill is to not panic: find the type parameters, their bounds, and where they appear in the parameters and return.
Interview angle
- "What are generics for?" Compile-time type safety and reuse without casts; errors move to the source line.
- "What is a raw type and why avoid it?" A generic type without arguments; it disables checking and pollutes typed collections.
- "Can you have a
List<int>?" No — type arguments must be reference types; useInteger(boxing) orint[]. - "What is erasure?" Type arguments are compile-time only; at run time
List<String>isList.
Key takeaways
- Generics move type errors from run time (casts) to compile time (the wrong line) and document what collections hold.
- Vocabulary: type parameter
E, type argumentString, parameterised typeList<String>, raw typeList. - Reference types only; the diamond
<>infers constructor arguments from the target. - Raw types are legacy — never in new code except class literals and
instanceof. - Erasure: generics are for the compiler; run time sees the raw class.
Common questions
What is a raw type in Java?
A raw type is a generic type used without type arguments, such as List instead of List<String>. It compiles only for compatibility with pre-generics code, gives "unchecked" warnings and switches off type checking, so a wrong element can pollute a typed list. Never write one in new code.
Can you have a List<int> in Java?
No. Type arguments must be reference types, so List<int> is a compile error; use List<Integer> and let autoboxing convert, or an int[]. That boxing cost is why IntStream and the primitive-specialised functional interfaces exist.
What is the diamond operator in Java?
The diamond, <>, tells the compiler to infer a constructor's type arguments from the target type: Map<String, List<Integer>> index = new HashMap<>(); compiles since Java 7. With var, put the arguments on the right instead, because var x = new HashMap<>() infers HashMap<Object, Object>.
What is the difference between a type parameter and a type argument?
A type parameter is the placeholder in a generic declaration, such as E in List<E>. A type argument is the actual type supplied when the generic type is used, such as String in List<String>, which makes it a parameterised type.
Why does Java use type erasure for generics?
For backward compatibility. Generics arrived in a language with ten years of compiled libraries and a JVM that knew nothing about them, so type arguments are checked by the compiler and then erased: at run time List<String> and List<Integer> are the same List class.
Exercises
Typed from the start
Read an integer n and n tokens. Put the integers into a List<Integer> and everything else into a List<String> (a token is an integer if matches("-?\\d+")). Print ints=<sum of the integers> and words=<the strings joined by ,> (or words= when there are none), then a third line same class=<whether the two lists' getClass() are ==> to observe erasure.
Example: 4 then 3 a 4 b →
ints=7
words=a,b
same class=trueA typed pair
Write class Pair<A, B> with first(), second(), Pair<B, A> swap() and a toString of (first, second). Read n lines <word> <int>; build a Pair<String, Integer> for each, print it, print its swap(), and add second() to a running total without a cast — the type parameter makes second() an Integer. Finally print total=<sum>.
Example: 2 then ab 3, cd 4 →
(ab, 3) swapped (3, ab)
(cd, 4) swapped (4, cd)
total=7In this module: Generics
← Checkpoint — Exceptions · Writing generic classes and methods →