Generics give compile-time type safety without runtime overhead, but that bargain comes with a list of things the language won't allow. Most of those restrictions trace back to a single fact: type information is erased before the bytecode runs, so anything that needs T to still exist at runtime is off the table. This lesson walks through the eight common restrictions, why each one exists, and the workaround that gets the job done.
List<int> doesn't compile. Type arguments to a generic class or method must be reference types, not primitives.
The compiler rejects this because every type parameter is replaced by its erasure at runtime. T erases to Object (or to its upper bound if it has one). A primitive int isn't an Object, so a slot that ultimately holds an Object reference can't hold a raw int value directly.
The fix is to use the wrapper class. Every primitive has one: int becomes Integer, double becomes Double, boolean becomes Boolean, and so on. Autoboxing and unboxing then make the code look like it stores primitives.
Every add(10) creates a new Integer object on the heap (modulo the small Integer cache for values from -128 to 127), and every loop iteration unboxes it back to int. For tight numeric loops over millions of values, a primitive int[] or a specialized primitive stream beats List<Integer>.
Inside a generic class or method, new T() doesn't compile. The compiler has no way to figure out which class's constructor to call, because by the time the code runs, T is gone.
The same problem applies to new T[10] and to any reflective trick that pretends to "ask T for its constructor". The type parameter is a compile-time symbol; at runtime, there's nothing to call new on.
There are two clean workarounds. The first is to accept a Supplier<T> from the caller. The caller knows the concrete type, so it can pass in a no-arg constructor reference.
The second option is to take a Class<T> token and call its getDeclaredConstructor().newInstance(). This works but is more verbose, can throw checked exceptions, and forces the type to have a no-arg constructor. Prefer Supplier<T> unless you specifically need the reflective form.
Java arrays are reified: they know their element type at runtime and check every store against it. Generics are erased: the type parameter is gone at runtime. Mixing the two would break the array's runtime check, so the compiler forbids new T[10] and new List<String>[10].
If a List<String>[] were allowed, it could be assigned to an Object[] and a List<Integer> put into it without any runtime check (because at runtime both lists are just List). That would let ArrayStoreException slip through the type system. Rejecting generic array creation closes the hole.
The practical workaround is to use ArrayList<T> (or another generic collection) instead of an array. The result is a resizable container with no casts.
When an array is required (for instance, to return one from a method whose signature can't change), the conventional workaround is to allocate an Object[] and cast it, suppressing the resulting unchecked warning.
This works because the array's true runtime type is Object[], and the cast to T[] is erased anyway. The downside is that data.getClass() reports Object[], not String[], so this trick is fine internally but dangerous when the array is returned to a caller who expects a true String[].
T.classInside a generic class, T.class is illegal as a way to get a Class object for the type parameter. The compiler has no Class<T> to provide, because T is erased before the bytecode runs.
This restriction is a direct consequence of erasure. T.class would have to be resolved at runtime, but at runtime T doesn't exist as a distinct entity. Inside the bytecode, T is replaced by its erasure (typically Object), and Object.class isn't what the caller wanted.
The workaround is to accept a Class<T> token as a constructor argument. The caller has the concrete class at the call site and can pass it in.
type.isInstance(raw) and type.cast(raw) are the runtime-safe equivalents of instanceof T and (T) raw. Both depend on the Class<T> token being passed in. This pattern is so common that the JDK uses it in Collections.checkedList, EnumMap, and several reflection APIs.
instanceof with Parameterized Typesinstanceof is a runtime check, and the runtime can't tell List<String> apart from List<Integer>. Both erase to the raw type List. The compiler refuses to pretend otherwise.
There are two legal forms. The first uses the unbounded wildcard List<?>, which means "a list of some unspecified type". The second uses the raw type List, which loses generic information entirely but is sometimes acceptable in legacy code.
The instanceof List<?> list form uses pattern matching for instanceof (Java 16+), which binds the matched value to a typed variable in one step. The variable list has type List<?>, so calling size(), isEmpty(), and reading elements as Object works, but adding anything to it doesn't (the wildcard says "some unknown type", and the compiler won't allow values to go into a list whose element type it can't verify).
This isn't a performance issue so much as a correctness one. Needing instanceof List<String> usually means the design has lost track of types somewhere upstream. Push type information into the API rather than recovering it at the receiving end.
TA type parameter belongs to an instance, not to the class. With class Cart<T>, every new Cart<String>() and new Cart<Integer>() shares the same Class<Cart> object (because of erasure), so a static field can't reasonably be of type T. Which T would it be?
The same rule applies to static methods that try to reference the class's T: they can't. They can declare their own independent type parameter, though, because that one is scoped to the method.
The usual workaround depends on the intent. For a per-class cache that doesn't care about T, declare the field as Object and cast when reading it (accepting the unchecked warning). For a per-type cache, use a Map<Class<?>, Object> keyed by class.
The static method registerDefault declares its own <U>, which is scoped to that method alone. It has nothing to do with the class's T, even though it lives inside Cart<T>. This is the standard escape hatch when a static method needs to be generic.
catch clauses match by runtime class. Because T is erased to its bound, the JVM has no way to tell whether a thrown exception's class matches a specific T. The compiler rejects the attempt.
The related restriction is that a generic class can't extend Throwable, directly or indirectly. That blocks the design class CartException<T> extends Exception, because then catch (CartException<String> e) would be ambiguous at runtime (both CartException<String> and CartException<Integer> would erase to the same CartException).
The standard workaround is to make the exception class non-generic and carry the type information as a regular field.
For the catch-by-T case, the same idea applies: catch the bound (often Exception) and inspect the caught exception at runtime to discriminate further.
Method overloading distinguishes methods by their parameter types. Two methods that only differ in their generic type arguments erase to the same signature at the bytecode level, so they look identical to the JVM. The compiler rejects them up front.
Both methods erase to void process(List), and two methods with the same erased signature can't coexist in the same class.
The fix is to give the two operations different names. Code is easier to read this way anyway, because the names can describe what each one does.
If the two methods truly need to share a name (say, because they implement a common interface contract), push the discrimination into a different mechanism: a Class<T> token, a dedicated wrapper type, or a method type parameter with a bound that's actually distinct after erasure.
Most of these decisions follow the same pattern: the language forbids something that depends on T existing at runtime, and the workaround is to pass the missing type information explicitly (as a Class<T> token or a Supplier<T>) or to use a structure that doesn't need T to be reified (a generic collection instead of an array, a non-generic exception with a Class<?> field).
The diagram is a lookup table. When one of the cyan-to-orange branches comes up, the green box on the right is the workaround. Three of the eight (Supplier, Class token, and generic collection) cover most cases between them.
10 quizzes