The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › Generics, enums, and annotations
Generics
What does the <Integer> in List<Integer> protect you from?
Generics are the syntax for writing "what this list holds" in the type, as in List<Integer>. When you write it, the compiler rejects code that adds a value of the wrong type, before the program runs. When you do not write it, that code compiles as it is, and a ClassCastException is thrown much later, at the place where the value is taken out.
Checked against real execution · All 20 combinations were actually compiled and run on JDK 21 and checked against the results
Concepts in this lab
- Generics
- This feature lets a class or method take the type it works with as a parameter. When you specify a type, as in List<String>, the type is checked at compile time, which prevents the mistake of adding a value of the wrong type and removes the need for casts. The type information is erased after compilation (type erasure), so at run time List<String> and List<Integer> cannot be told apart.
- Raw type
- This is a generic class, one that lets you write a type as in List<String>, written as just List, without <...>. It is a form that was kept so that code from before Java 5, when there were no generics, can still be used. The compiler cannot check what is added through a raw type variable and gives only an unchecked warning, so even code that adds a String to the list that a List<Integer> refers to compiles. That mistake shows up later as a ClassCastException at the place where the value is taken out. It is not used in newly written code, and when the element type is unknown, List<?> is used.
- Type erasure
- This is what the compiler does when it uses the type arguments of generics (the Integer in List<Integer>) only for the checks at compile time and does not keep them in the code that runs. As a result, at run time a List<String> and a List<Integer> are objects of the same class, and a list object does not remember what type it was meant to hold. At the places where values are taken out, the compiler inserts the casts for you. Because of this, you cannot ask a list for its element type at run time, and you cannot create an object from a type parameter, as in new T(). If you bypass the check of the compiler with a raw type and add a wrong value, nothing stops it at run time.
- Wildcard (? extends and ? super)
- This is the syntax that uses ? inside the <> of a generic type to express that "the type is not fixed". List<? extends Number> accepts a list of Number or of any subtype of Number, and a value taken out of it can be used as a Number, but no value can be added to it. List<? super Integer> accepts a list of Integer or of any supertype of Integer, and an Integer can be added to it, but a value taken out of it can be received only as an Object. Because a List<Integer> cannot be assigned to a List<Number> variable, wildcards are needed when you write a method that accepts lists of several types. Use extends for a parameter that only has values taken out of it, and super for a parameter that only has values put into it.
Try these
- Default settings — A string is added without generics. The code compiles, and an exception is thrown later, when the value is taken out.
- With generics — The same mistake is caught in advance as a compile-time error.
- Assigning to List<Number> — Integer is a subtype of Number, and yet the assignment itself does not compile.
- Adding to ? extends — You can get values, but you cannot add even an Integer.
- Getting from ? extends — Getting a value as a Number works.
- Adding to ? super — You can add an Integer.
- Getting from ? super — The value cannot be received as a Number. It can be received only as an Object.
Running with the default settings
- List<Integer> ints is created, and 1 and 2 are added: <Integer> is a type argument that says "this list holds only Integer". The compiler reads this type argument and checks the code that adds values to ints and gets values from it.
- List list = ints is checked: The same list is now also referred to by the variable list, which is declared without a type argument (<...>). A generic type used this way, without type arguments, is called a raw type. It is a form that was kept so that code from the time before generics can still be used, so the assignment compiles. In return, the compiler cannot check what is added to list.
- list.add("three") is checked: list is a raw type, so the compiler cannot check the type of the value that is added. It gives only a warning, not an error ("unchecked call to add(E) as a member of the raw type List"). This code adds a String to a list of Integer, and yet it compiles.
- Compilation succeeded: A warning does not stop compilation. The program now runs.
- list.add("three") runs: The list object at run time does not remember that it is meant for Integer. Nothing is checked when a value is added, so the String value (three) goes in as it is. list and ints are the same object, so a String has gone into ints, which is meant to hold only Integer.
- System.out.println(ints) runs: It prints the contents of the list. The output is [1, 2, three]. Printing does not care what type the elements are, so nothing happens yet.
- ints.getClass() == strs.getClass() runs: It prints true. At run time, a List<Integer> and a List<String> are objects of the very same ArrayList class. <Integer> and <String> are used only for the checks at compile time and do not remain in the code that runs (type erasure).
- Integer last = ints.get(ints.size() - 1) runs: A ClassCastException is thrown. ints is a List<Integer>, so the compiler inserted a cast on this line that "converts the value taken out to an Integer". But the value that actually comes out is a String. The failure happens not on the line that added the wrong value but much later, on the line that takes it out, so the cause is hard to find.
Result of this run
- Without generics (a raw type), code that adds a value of the wrong type compiles, nothing happens at the moment the value is added, and then a ClassCastException is thrown on a different line, the one that takes the value out. Change the declaration to List<Integer>, and the same code becomes a compile-time error.
- The true in the output shows type erasure. A list at run time does not remember its element type, so all the protection that generics give happens at compile time. If you bypass that check with a raw type, nothing is left to stop the mistake.
Code
List<Integer> ints = new ArrayList<>();
ints.add(1);
ints.add(2);
List list = ints;
list.add("three");
System.out.println(ints);
List<String> strs = new ArrayList<>();
System.out.println(ints.getClass() == strs.getClass());
Integer last = ints.get(ints.size() - 1);
System.out.println("last value: " + last);
Assumptions of this model
All 20 combinations were actually compiled on JDK 21 on this PC (javac -Xlint:unchecked) from a source file that places the code on the screen inside a main method, and those that compiled were run, and the compilation result, the line and the message of each error, the warning message, the output, the kind of exception, and the line where the exception was thrown were checked against the results. The compiler messages are written in the form that the command-line javac shows (class names only, and CAP#1 for an unknown type). The code on the screen omits import java.util.ArrayList, import java.util.List, and the declaration of the main method. When you compile without -Xlint:unchecked, you get only the note "uses unchecked or unsafe operations" in place of the details of the warning. null can be added even to a list declared with ? extends, but the model does not cover this. Even though type erasure happens, the generic information written in the declarations of fields and methods remains in the class file and can be read through reflection. What is erased is the information that an object has at run time.
Previous lab: Checked and unchecked exceptions · Next lab: Enum classes