RizTech Academy logo
RizTech Academy
GenericsLesson 1 of 325 min

Why generics exist, and the raw-type trap

You have been reading angle brackets since module 2 — String[] args is not one, but List<String> is, and Arrays.asList returned one. This module explains them properly, because an intern reading real Java meets <T extends Comparable<? super T>> in their first week and needs to not flinch.

Start with what life was like without them, because generics are a solution and the problem is more memorable than the answer.

Java before 2004

Collections held Object. Everything you took out needed a cast.

List names = new ArrayList();
names.add("Priya");
names.add("Arjun");
names.add(42);

for (Object o : names) {
    String s = (String) o;
    System.out.println(s.toUpperCase());
}
PRIYA
ARJUN
threw: 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')

Two names printed, then a crash. The mistake was on line four — names.add(42) — and the failure happened somewhere else entirely, in a loop that looks correct. In a real system the add is in one file, the loop is in another, and the crash is in production.

That is a List without a type argument, called a raw type. It is still legal, for backwards compatibility, and the compiler is not happy about it:

warning: [rawtypes] found raw type: List
warning: [unchecked] unchecked call to add(E) as a member of the raw type List

Never write a raw type. If you see one in existing code, adding the type argument is usually a five-second fix that turns a future runtime crash into a compile error today.

With the type argument

List<String> names = new ArrayList<>();
names.add("Priya");
names.add(42);
error: no suitable method found for add(int)
        names.add(42);
             ^
    method List.add(String) is not applicable
      (argument mismatch; int cannot be converted to String)

The error is now on the line with the mistake, at compile time, before the program has ever run. And the loop needs no cast:

for (String s : names) {
    System.out.println(s.toUpperCase());
}

That is the whole value proposition: generics move a class of error from runtime to compile time, and remove the casts that hid it.

new ArrayList<>() with empty brackets is the diamond operator. The compiler infers the type argument from the left-hand side, so you write it once.

Generics are not covariant, and that is deliberate

The arrays lesson ended with this:

Object[] objects = new String[2];
objects[0] = 42;
java.lang.ArrayStoreException: java.lang.Integer

Compiles, fails at runtime. Arrays are covariant — a String[] is accepted where an Object[] is wanted — and the JVM has to check every store to catch the consequences.

Generics learned from that:

List<Object> everything = new ArrayList<String>();
error: incompatible types: ArrayList<String> cannot be converted to List<Object>

List<String> is not a List<Object>, even though String is an Object. This is called invariance, and it is the single most confusing thing about generics until you see why.

If the assignment were allowed, everything.add(42) would be perfectly legal — everything is a List<Object>, after all — and you would have put an Integer into a List<String>. The compiler refuses the first step so the second cannot happen. Arrays allow the first step and pay for it with a runtime check.

That invariance is inconvenient often enough that Java provides an escape hatch, which is the wildcards lesson at the end of this module. Knowing why it exists makes that lesson short.

Type erasure, and what it costs

Generics are a compile-time feature. After type checking, the compiler erases the type arguments and inserts casts where needed. The bytecode has no idea your list was a list of strings:

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());
System.out.println(a.getClass().getName());
true
java.util.ArrayList

Same class at runtime. This was done so that Java 5 code could interoperate with the billions of lines written before it, and it is the reason for every remaining oddity in this module.

Four consequences worth knowing, because each produces a puzzling error:

You cannot Why What to do instead
new T[n] The array needs a real type at runtime (T[]) new Object[n], or use a List<T>
o instanceof List<String> The type argument is gone o instanceof List<?>
new T() No constructor to call Pass a Supplier<T> or a Class<T>
Overload on List<String> and List<Integer> Both erase to List Give the methods different names
error: generic array creation
        return new T[n];
               ^

error: Object cannot be safely cast to List<String>
        System.out.println(o instanceof List<String>);
                           ^

The second message is worth reading closely: the compiler is not saying the test is wrong, it is saying it cannot be checked. o instanceof List<?> compiles, because that asks a question erasure can still answer.

One useful exception, since Java 16: x instanceof List<String> is allowed when x is already statically known to be a List<String>, because then nothing needs checking. That is why the same line compiles in one place and not another.

The vocabulary

Worth fixing now, because the rest of the module uses it.

Term Means Example
Generic type A type with a type parameter List<E>
Type parameter The placeholder in the declaration E in interface List<E>
Type argument The actual type supplied String in List<String>
Raw type A generic type used with no argument List
Parameterised type A generic type with its argument List<String>
Bounded type parameter A parameter with a restriction <T extends Number>
Wildcard An unknown type argument List<?>, List<? extends Charge>

And the conventional single-letter names, which you should follow because every Java reader expects them:

Letter Used for
T Type
E Element, in a collection
K, V Key and value, in a map
R Return type
N Number
S, U Second and third types

What generics do not do

  • They do not exist at runtime. You cannot ask a list what it holds.
  • They do not work with primitives. List<int> is illegal; it is List<Integer>, and every element is a boxed object. For large numeric data this matters, and IntStream in module 6 is part of the answer.
  • They do not make code faster. The casts they remove from your source are still generated. The gain is correctness and readability.
  • They do not replace validation. List<String> guarantees strings, not non-empty, non-null, six-digit strings.

Check your work

What is a raw type, and what is wrong with it? A generic type used without a type argument, like List. It compiles with warnings, allows anything to be added, and converts a compile-time error into a ClassCastException somewhere unrelated.

Where did the ClassCastException appear, relative to the mistake? In the reading loop, not on the add(42) line that caused it. That distance is the cost.

Why is List<Object> l = new ArrayList<String>() rejected? Because it would then be legal to add an Integer through l, putting a non-String into a List<String>. Generics are invariant so that cannot happen.

Why do arrays allow the equivalent? Arrays are covariant, a decision from before generics existed. The JVM pays for it with a runtime check that throws ArrayStoreException.

What is type erasure? The compiler checks types, then removes the type arguments, so at runtime a List<String> and a List<Integer> are both just ArrayList.

Name two things erasure prevents. new T[n] (generic array creation), o instanceof List<String> on a variable not already known to be one, new T(), and overloading on two parameterisations of the same type.

Why is List<int> illegal? Type arguments must be reference types. Use List<Integer> and accept the boxing, or a primitive-specialised API.

Practice 2, the raw-type conversion. Adding <String> moves the failure from a ClassCastException in the loop to no suitable method found for add(int) on the offending line, and lets you delete the cast from the loop entirely. The number of lines goes down and the number of bugs findable by the compiler goes up.

Practice 4, the erasure experiments. a.getClass() == b.getClass() is true; both print java.util.ArrayList. new T[n] gives generic array creation. o instanceof List<String> on an Object gives Object cannot be safely cast to List<String>, while o instanceof List<?> compiles — because that is a question the runtime can still answer.

Practice

  1. Reproduce the old world. Write the raw List example, add a number to a list of names, and watch where the ClassCastException appears. Note how many lines separate the cause from the crash.

  2. Convert it. Add the type argument and recompile. Read the new error, then delete the cast in the loop and confirm it still compiles.

  3. Turn warnings on. Compile the raw version with javac -Xlint:all and read all five warnings. Then find out how to make your IDE show them permanently. Most raw types in real code exist because nobody had warnings on.

  4. Poke at erasure. Print a.getClass() == b.getClass() for a List<String> and a List<Integer>. Then try to write a method returning new T[n], and try o instanceof List<String> where o is an Object. Read both errors, then make the second one compile.

  5. Prove the covariance difference. Write the Object[] objects = new String[2] version and watch it throw at runtime. Then write the List equivalent and watch it fail to compile. Write one sentence on which you would rather debug.

  6. Harder — find a raw type in the wild. Open any Java project on GitHub that is more than ten years old and search for new HashMap() or implements Comparator without brackets. Pick one, work out what type argument it should have, and write down what would have to be true for the missing one to cause a bug. This is the commonest real refactoring an intern is handed in their first month.

Next: writing your own generic classes and methods, rather than only consuming other people's.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship