Before you begin
Why this matters
When a generic list holds a wrong element, the program fails at a line that only reads it, often far from the line that caused it. If you know which checks javac makes and which checks happen while the program runs, you can find that cause, and you can write generic code that checks types where it must.
What this helps you reason about
- Predict which line throws a ClassCastException when a List<String> holds an Integer.
- Explain why a String[] refuses an Integer while the program runs, and why a List<String> cannot.
- Pass a String.class argument when generic code must check, cast, or make a value of its type.
Generics and type parameters showed that javac checks List<String> and then compiles it as a plain List. This lesson follows what that means while the program runs. C# keeps List<int> and List<string> apart at run time, and TypeScript keeps no types at all. Java is in between: every object keeps its class, but no object keeps a type argument. That explains where a ClassCastException appears, why a String[] refuses a number that a List<String> accepts, and why some generic code needs a String.class argument.
Already know this? The short version
- Type arguments are erased: a running List<String> is an ArrayList, each element knows its own class, and instanceof can test List<?> but not List<String> (error: Object cannot be safely cast to List<String>).
- A raw type or an unchecked cast such as (List<String>) stored causes heap pollution; javac inserts a cast in the calling code wherever a read must give a String, so the ClassCastException names that read, not the cause.
- Generic code cannot test instanceof T or write new T(); pass a Class<T> and use isInstance, cast, or java.lang.reflect.Array.newInstance, or pass a Supplier<T>; Collections.checkedList refuses a wrong element at the add.
- Arrays keep their element class, so a String[] used as an Object[] throws ArrayStoreException on a wrong store; a List<String> is not a List<Object>, wildcards (? extends for reading, ? super for adding) give back flexibility, and new T[n] is a generic array creation error.
- The section “What goes wrong with erasure in real programs?” covers casting toArray(), overloads with the same erasure, unchecked casts of values read as Object, and tables keyed by Class.
Words in this lesson (7)
- type erasure
- Removing type arguments when a program is compiled. javac checks List<Integer>, then compiles it as a plain List, so the running program has one class for every type argument.
- raw type
- A generic class used without type arguments, such as List instead of List<Integer>. javac does not check its elements, and only warns.
- cast
- An expression such as (Integer) value that tells the compiler to treat a reference as a more specific type. The JVM checks it while the program runs and throws a ClassCastException when the object has another type.
- unchecked cast
- A cast to a generic type, such as (List<String>) value or (T) value. While the program runs, the JVM checks only the class, List or Object, never the type argument, so javac warns.
- heap pollution
- A variable of a generic type that refers to an object of another type, such as a List<String> that refers to a list of Integers. A raw type or an unchecked cast causes it, and a later read fails.
- Class object
- The object that describes one class while the program runs. String.class is the Class object for String, and its type is Class<String>; its methods isInstance and cast check a value against that class.
- wildcard
- A ? in a type argument, meaning one type that is not known. List<?> is a list of some type; List<? extends Number> is a list of Number or of a subclass of Number.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / What is left
What does a running program know about a List<String>?
Type erasure removes the type arguments, but not the objects’ classes. Each object keeps its own class for its whole life. A List<String> made with new ArrayList<>() is an ArrayList, and getClass().getName() returns "java.util.ArrayList". Each element also keeps its class: "Hawks" is a String, and 3 in a List<Integer> is an Integer object. What is missing is the type argument. No object records that the list was made to hold Strings. The stepper shows the same: a list appears as java.util.ArrayList@ with an id and its elements, never with <String>.
So instanceof can ask about the class of the list, but not about its type argument. value instanceof List<?> list is allowed. The ? is a wildcard: List<?> means a list of some type that is not known, so the test checks only that value is a List. value instanceof List<String> list does not compile, and javac reports:
error: Object cannot be safely cast to List<String>
To learn what a list holds, test its elements one by one, as allText does below. Inside generic code the same rule applies to T: value instanceof T reports error: Object cannot be safely cast to T, and new T() does not compile either, because no T exists while the program runs.
static boolean allText(Object value) {
if (!(value instanceof List<?> list)) {
return false;
}
for (Object item : list) {
if (!(item instanceof String)) {
return false;
}
}
return true;
} While the program runs, a list knows its class and each element knows its own class; no object knows the type argument.
Coming from C#: C# keeps the type argument at run time, so typeof(T) and is List<string> work; in Java neither exists, and a list object knows only that it is an ArrayList.
Precisely: reifiable types and what reflection keeps
The Java Language Specification (4.7) calls a type reifiable when it is fully available while the program runs: primitives, classes that are not generic, raw types such as List, wildcard types such as List<?>, and arrays of these. instanceof needs a reifiable type, with one exception added in Java 16: the test is allowed when the static type already makes it safe, so a Collection<String> can be tested with instanceof List<String> l. Erasure removes type arguments from objects, not from declarations. A class file still records that a field is declared List<String>, and reflection can read it (Field.getGenericType). Some libraries use this to recover a type argument, by making a small subclass of a generic class.
Experiment
Ask two lists what they hold
Type arguments are erased, so each list object knows only its class, ArrayList. instanceof List<?> checks that class. Each element still knows its own class, so instanceof String works on an element. A test for List<String> would need the type argument, which no object has, and javac refuses to compile it.
Why it matters To learn what a list holds while the program runs, test its elements, not the list.
Predict the three printed lines. Then change value instanceof List<?> list to value instanceof List<String> list and predict whether the program still runs.
import java.util.ArrayList;
import java.util.List;
class Main {
static boolean allText(Object value) {
if (!(value instanceof List<?> list)) {
return false;
}
for (Object item : list) {
if (!(item instanceof String)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
List<String> teams = new ArrayList<>(List.of("Hawks", "Lynx"));
List<Integer> goals = new ArrayList<>(List.of(3, 1));
System.out.println(teams.getClass().getName());
System.out.println(goals.getClass().getName());
System.out.println(allText(teams) + " " + allText(goals));
}
}
What to look for
teams shows java.util.ArrayList@ and an id, then ["Hawks", "Lynx"]. goals shows another ArrayList id with two Integer objects, such as java.lang.Integer@64 {value: 3}. Neither shows <String> or <Integer>. The first two lines print java.util.ArrayList. Step into allText for teams: list shows the same id as teams, and item takes "Hawks", then "Lynx". For goals, item is the first Integer, and the method returns false at once. The run prints true false. After the edit, nothing runs, and javac reports: Main.java:6: error: Object cannot be safely cast to List<String>.
Expected output
java.util.ArrayList
java.util.ArrayList
true false02 / Heap pollution
How does a wrong element get into a List<String>?
javac checks every add on a List<String>. So an Integer can reach a List<String> only through code that javac cannot check. There are two ways.
The first is a raw type, such as List without type arguments, which turns the element checks off. Generics and type parameters used one to add "red" to a List<Integer>.
The second is an unchecked cast: a cast to a generic type, such as (List<String>) stored, where stored is an Object. While the program runs, the JVM can check only that stored is a List. The cast succeeds for any list, whatever it holds. javac compiles both and adds a warning. Without extra options, javac prints one short note that the file uses unchecked or unsafe operations; the stepper does not show it. @SuppressWarnings("unchecked") above a method hides the warning. Write it only where you have checked the types yourself.
A variable of a generic type that refers to an object of another type is heap pollution. Below, teams is a List<String> that refers to a list of Integers. Nothing fails yet.
The failure comes later. For a read whose result must be a String, javac inserts a cast to String in the calling code. Inside a generic method such as last, T is compiled as Object, so the Integer passes through it with no check. Object seen = last(teams) needs no cast, and works. String team = last(teams) gets a cast, and the run stops:
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
The exception points at the reading line, not at the unchecked cast. To find the cause, look for the raw type or the unchecked cast that let the element in.
static <T> T last(List<T> items) {
return items.get(items.size() - 1);
}
List<Integer> goals = new ArrayList<>(List.of(3, 1));
Object stored = goals;
// unchecked: checks only that stored is a List
List<String> teams = (List<String>) stored;
// works: no cast to String is needed
Object seen = last(teams);
// ClassCastException here, not on the cast line
String team = last(teams); A raw type or an unchecked cast lets a wrong element in; the ClassCastException appears later, at a line that reads the element as its declared type.
Coming from TypeScript: TypeScript erases its types and checks nothing at run time; Java erases type arguments but keeps each object’s class, so a cast still fails, only later than you expect.
Precisely: where javac puts the casts
The Java Language Specification defines heap pollution in 4.12.2 and unchecked conversion in 5.1.9. javac -Xlint:unchecked lists every unchecked line instead of the one note. The inserted casts are checkcast instructions, which javap -c Main shows. A cast is inserted wherever the erased type, Object, is not enough: an assignment to String, a method call such as teams.get(0).length(), and also System.out.println(teams.get(0)), because javac chooses println(String) for a String argument. String concatenation with + also gets the cast. A generic method with a varargs parameter of type T... can cause heap pollution too, which is why javac warns about it unless the method is marked @SafeVarargs.
Experiment
Follow an Integer through a List<String>
The cast (List<String>) stored checks only that stored is a List, so teams refers to a list of Integers. Inside last, T is compiled as Object, so returning an Integer needs no check. javac inserts a cast to String only in the caller, and only where the result must be a String. Object seen needs no cast, so it works. String team gets the cast, which fails.
Why it matters A ClassCastException from a generic list names the line that read the element. The cause is an earlier raw type or unchecked cast.
Predict both printed lines. Then add the line String team = last(teams); just before the last println, and predict which line fails.
import java.util.ArrayList;
import java.util.List;
class Main {
static <T> T last(List<T> items) {
T item = items.get(items.size() - 1);
return item;
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
List<Integer> goals = new ArrayList<>(List.of(3, 1));
Object stored = goals;
List<String> teams = (List<String>) stored;
Object seen = last(teams);
System.out.println("seen " + seen);
System.out.println(teams.size());
}
}
What to look for
After the cast line, goals, stored, and teams all show the same ArrayList id with two Integer objects: one list, three names. Step into last: items is that list, and item shows java.lang.Integer@ with {value: 1}, although its declared type is T. The method returns without an error. Back in main, seen shows the same Integer id. The run prints seen 1, then 2. After the edit, last runs a second time without an error, and the run stops on the new line in main with java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String. The next println never runs.
Expected output
seen 1
203 / Class objects
How can generic code check a type while it runs?
Generic code cannot test value instanceof T, and a cast (T) value is unchecked: it checks nothing while the program runs. A method that must check a type needs the class itself, passed as a value.
String.class is a class literal: an expression whose value is the Class object for String, the object that describes the class while the program runs. The type of String.class is Class<String>. So a parameter Class<T> type ties the class to T, and the caller chooses both with one argument, such as String.class. Such a parameter is often called a type token.
A Class object can do what T cannot. type.isInstance(value) is an instanceof test. type.cast(value) is a checked cast: it returns the value as a T, or throws a ClassCastException at once, with a message such as Cannot cast java.lang.String to java.lang.Integer.
The standard library uses the same idea. Collections.checkedList(list, String.class) returns a list that keeps String.class and checks every element added through it, even through a raw type. The experiment below shows the wrong element refused at the line that adds it.
To make a new T, which new T() cannot do, take a function that makes one, such as a Supplier<T> (see Java lambdas and effectively final locals).
static <T> T firstOf(List<?> items, Class<T> type) {
for (Object item : items) {
if (type.isInstance(item)) {
return type.cast(item);
}
}
return null;
}
// "Hawks": the first element that is a String
String team = firstOf(List.of(3, "Hawks"), String.class); When generic code must check a type while it runs, pass a Class<T> and use isInstance and cast; (T) value checks nothing.
Coming from Kotlin: Kotlin’s inline functions can declare reified T and test value is T; Java has no reified type parameters, so the caller passes String.class instead.
Precisely: limits of a Class token
A Class object exists only for a reifiable type: List.class is allowed, List<String>.class is not, so a token cannot describe a list of Strings. int.class exists, but its isInstance returns false for every object, because no object is an int; pass Integer.class for boxed values. A Class object can also make an object, with type.getDeclaredConstructor().newInstance(), which fails while the program runs when the class has no constructor without parameters. A Supplier<T> is checked by javac and usually the better choice.
Experiment
Add an Integer to a checked list
checkedList returns a list that keeps String.class in its type field. Its add method calls the Class object’s check before it passes the element on to plain. A raw type skips javac’s check but not this one, because this check runs inside add while the program runs.
Why it matters When a list may be reached through a raw type or an unchecked cast, a checked list moves the error to the line that adds the wrong element.
Predict both printed lines. Then change Collections.checkedList(plain, String.class) to plain and predict again.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Main {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
List<String> plain = new ArrayList<>();
List<String> teams = Collections.checkedList(plain, String.class);
teams.add("Hawks");
List raw = teams;
try {
raw.add(7);
System.out.println("added");
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
System.out.println(plain);
}
}
What to look for
plain shows java.util.ArrayList@ and an id. teams shows java.util.Collections$CheckedRandomAccessList@ and another id, with three fields: list and c both lead to plain, with the same id, and type is a java.lang.Class object. raw shows the same id as teams. Step from raw.add(7): the next stop is the catch line, so println("added") never runs. e is a ClassCastException. The panel lists its internal fields, such as backtrace; its message is in detailMessage. type now shows name: "java.lang.String" (Java fills in that field the first time the name is used, so it showed null before). The run prints Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String, then [Hawks]. After the edit, teams and raw show plain’s id, raw.add(7) succeeds, plain shows "Hawks" and an Integer holding 7, and the run prints added, then [Hawks, 7].
Expected output
Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String
[Hawks]04 / Arrays
Why does an array refuse a wrong element when a list does not?
Arrays work differently. An array object keeps its element class for its whole life. The stepper shows it in the name: java.lang.String[]@ and an id. A String[] can be used where an Object[] is expected, because every String is an Object. So the variable type does not tell the JVM which elements are allowed, and the JVM checks every store into an array of references while the program runs. Storing an Integer into a String[] through an Object[] variable throws ArrayStoreException at that line, with the message java.lang.Integer.
Generic lists make the opposite choice. A List<String> cannot be used as a List<Object>; javac reports:
error: incompatible types: List<String> cannot be converted to List<Object>
If it could, code holding the List<Object> could add an Integer, and the list object, which does not know its type argument, could not refuse it.
A wildcard gives some of that flexibility back. A List<? extends Number> accepts a List<Integer> or a List<Double>, and get returns a Number. Adding is refused, because the real element type is unknown: src.add(5) reports error: incompatible types: int cannot be converted to CAP#1, where CAP#1 is javac’s name for that unknown type. A List<? super Integer> is the opposite. It accepts a List<Integer>, a List<Number>, or a List<Object>; add(3) is allowed, and get returns only an Object. So ? extends suits a list you read from, and ? super a list you add to.
The two models do not mix. new T[n] and new List<String>[3] both report error: generic array creation, because the array would need an element class that does not exist while the program runs. A method that must make a T[] needs the class from its caller: java.lang.reflect.Array.newInstance(type, n) makes an array of n elements whose element class is type. It returns an Object, so the method casts it with (T[]). That cast is unchecked, and here it is safe, because the array was made from type.
String[] squad = {"Ada", "Bo"};
Object[] slots = squad; // allowed
slots[1] = 7; // ArrayStoreException
List<String> players = new ArrayList<>();
// List<Object> all = players; would not compile
// get gives an Object; add is refused
List<? extends Object> view = players; An array checks each store while the program runs; a generic list is checked by javac and not at all while the program runs.
Precisely: covariance and its cost
The Java Language Specification (10.5) requires the ArrayStoreException check for every store into an array whose element type is a reference type. Arrays of primitives need no check. Accepting a String[] as an Object[] is called covariance; lists are invariant, and wildcards add covariance (? extends) or contravariance (? super) one use at a time. A varargs parameter is an array, so a generic varargs parameter T... is created as an array of the erased type, which is why javac warns about it. List.toArray(IntFunction) and Array.newInstance are the two usual ways to get an array of the right class.
Experiment
Store a number in a String array
An array object keeps its element class, here String, for its whole life. The variable type Object[] is allowed, because every String is an Object, so the JVM checks each store into the array while the program runs. 7 is boxed to an Integer, which is not a String, so the store throws ArrayStoreException. A real Object[] accepts any object.
Why it matters An array refuses a wrong element at the line that stores it; the check uses the array object’s class, not the variable’s type.
Predict the three printed lines. Then change Object[] slots = squad; to Object[] slots = {"Ada", "Bo"}; and predict again.
class Main {
public static void main(String[] args) {
String[] squad = {"Ada", "Bo"};
Object[] slots = squad;
System.out.println(slots.getClass().getSimpleName());
try {
slots[1] = 7;
System.out.println("stored");
} catch (ArrayStoreException e) {
System.out.println("refused " + e.getMessage());
}
System.out.println(slots[1]);
}
}
What to look for
squad shows java.lang.String[]@ and an id, then ["Ada", "Bo"]. slots shows the same id and the same class, String[], although its variable type is Object[]. The first line prints String[]. Step from slots[1] = 7: the next stop is the catch line, and slots still shows ["Ada", "Bo"]. The run prints refused java.lang.Integer, then Bo. After the edit, slots shows java.lang.Object[]@ and a different id. slots[1] = 7 succeeds, slots shows "Ada" and an Integer holding 7, squad keeps its two names, and the run prints Object[], stored, then 7.
Expected output
String[]
refused java.lang.Integer
Bo05 / In production
What goes wrong with erasure in real programs?
Each bug below follows from erasure, and each one compiles.
toArray() on a List<String> returns an Object[], because the list does not know its element type. (String[]) players.toArray() compiles and throws ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;. [Ljava.lang.String; is the JVM’s name for String[]. Pass the array type instead: players.toArray(new String[0]), or players.toArray(String[]::new), where String[]::new is a function that makes a String[] of a given length.
Two overloads that differ only in a type argument cannot both exist. total(List<String> names) and total(List<Integer> goals) erase to the same total(List), and javac reports error: name clash: total(List<String>) and total(List<Integer>) have the same erasure. Give the methods different names.
Values read from a Map<String, Object>, a cache, or a decoded file come back as Object, and (List<String>) value compiles with only a warning. If the stored list held something else, the program fails much later, in another method. Check the elements where the value comes in, or copy them into a new list after testing each one.
A table keyed by Class, such as a Map<Class<?>, Formatter>, has one key, List.class, for every kind of list. A formatter for lists of names and one for lists of goals replace each other. Key such a table by your own names for the full types.
// Object[]: the list does not know its element type
Object[] copy = players.toArray();
// ClassCastException
String[] wrong = (String[]) players.toArray();
// String[]: the argument gives the class
String[] names = players.toArray(new String[0]); Pass an array or a Class where the class matters, give overloads different names, and check values typed as Object where they come in.
Experiment
Cast the result of toArray
The list does not know its type argument, so toArray() can only make an Object[]. The cast checks the array object’s class, Object[], which is not String[]. toArray(new String[0]) receives an array whose class is String[], so the list can make a new array of that class and copy the names into it.
Why it matters To get a typed array from a list, pass an array or a function that makes one; never cast the result of toArray().
Predict both printed lines. Then change (String[]) players.toArray() to players.toArray(new String[0]) and predict again.
import java.util.List;
class Main {
public static void main(String[] args) {
List<String> players = List.of("Ines", "Tom");
Object[] copy = players.toArray();
System.out.println(copy.getClass().getSimpleName());
try {
String[] names = (String[]) players.toArray();
System.out.println(names.length);
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
}
}
What to look for
players shows java.util.ImmutableCollections$List12@ and an id, a class Java uses for small lists that cannot change, with ["Ines", "Tom"]. After the next line, copy shows java.lang.Object[]@ with the same two names. The first line prints Object[]. Step from the cast line: the next stop is the catch line. The run prints class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;, followed by a note in parentheses about the module of both classes. After the edit, names shows java.lang.String[]@ with ["Ines", "Tom"], and the run prints Object[], then 2.
Expected output
Object[]
class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')06 / Try it
Which line fails?
One list is reached through two variables of different generic types. Decide where each value is checked.
import java.util.ArrayList;
import java.util.List;
class Main {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
List<Integer> quarters = new ArrayList<>(List.of(4, 5));
Object stored = quarters;
List<String> notes = (List<String>) stored;
notes.add("timeout");
System.out.println(quarters.size());
Object last = quarters.get(2);
System.out.println(last);
try {
int goals = quarters.get(2);
System.out.println(goals);
} catch (ClassCastException e) {
System.out.println("read failed");
}
}
}
What are the three printed lines?
Reveal the reasoning
3, then timeout, then read failed. The cast to List<String> checks only that stored is a List, so notes and quarters refer to one ArrayList, and notes.add("timeout") puts a String into it. Nothing checks the add, so the size is 3. Object last = quarters.get(2) needs no cast, so the String is printed. int goals = quarters.get(2) needs an Integer to unbox, so javac inserted a cast to Integer on that line, and it throws.
Practice tickets
- JAVA-501 · The triathlon finish screen crashes after a missed chip read → Free
- JAVA-502 · The netball match clock crashes when a setting was typed as text → Sign in · Pro
- JAVA-503 · Every orienteering watch screen crashes after the race log change → Sign in · Pro
Keep exploring
Functions as values and higher-order operations →
Primary references
- Java Language Specification: Type Erasure ↗
- Java Language Specification: Reifiable Types ↗
- Java Language Specification: Heap Pollution ↗
- Java Language Specification: Unchecked Conversion ↗
- Java Language Specification: Array Store Exception ↗
- Java Language Specification: Type Comparison Operator instanceof ↗
- The Java Tutorials: Restrictions on Generics ↗
- java.lang.Class (Java SE 17) ↗
- java.util.Collections.checkedList (Java SE 17) ↗
- java.lang.reflect.Array (Java SE 17) ↗
- java.util.List.toArray (Java SE 17) ↗