skip to content

Generic Restrictions

Everything erasure and the language design forbid — primitives, new T, generic arrays, parameterized instanceof, generic exceptions — plus the type-token workarounds. Interviewers ask why in each case, and the answer is almost always erasure.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

Why can't you write List<int> in Java, and what must you use instead?

level: juniorimportance: must knowfreq 70%

answer

  1. Generics take reference types only
  2. int -> Integer wrapper
  3. Autoboxing bridges the gap
  4. Erasure stores elements as Object
  5. Cost: heap objects + boxing + NPE on null

basics

~10 s

Generic type arguments must be objects, and int is a primitive, not an object. So List<int> is illegal. Use the wrapper class instead: List<Integer>. Java then converts int to Integer for you automatically.

solid answer

~40 s

Java generics only accept reference types (objects) as type arguments, never primitives like int, double, or boolean. That's why List<int> won't compile. The reason is implementation: generics are erased to Object at compile time, and Object can only hold references, not raw primitive values. So you use the wrapper class, List<Integer>, and rely on autoboxing/unboxing: writing list.add(5) auto-wraps the int 5 into an Integer, and int x = list.get(0) auto-unwraps it. The trade-off is that each element is a separate heap object with pointer overhead, and boxing/unboxing in hot loops costs CPU and allocations. For performance-critical primitive collections, people reach for int[] arrays or libraries like Eclipse Collections / fastutil.

code

java · 9 lines
java
// List<int> nums = new ArrayList<>(); // does NOT compile

List<Integer> nums = new ArrayList<>();
nums.add(5);              // autobox: int -> Integer.valueOf(5)
int first = nums.get(0); // auto-unbox: Integer -> intValue()

// Watch out for null unboxing:
Integer boxed = null;
// int oops = boxed;     // throws NullPointerException at runtime

go deeper

for a junior

Knows List<int> is illegal and that you write List<Integer> instead, relying on autoboxing.

for a middle

Can explain it's because generics require reference types and that boxing has memory/CPU costs and a null-unboxing NPE risk.

for a senior

Ties the restriction to type erasure (elements stored as Object), and knows the alternatives (int[], primitive streams, fastutil/Eclipse Collections).

for a principal

Frames it as a language-implementation trade-off, can reason about allocation/cache impact at scale, and references Project Valhalla as the direction for primitive/value generics.

## The terms first **Primitive type:** Java has eight built-in value types — `byte`, `short`, `int`, `long`, `float`, `double`, `char`, `boolean`. A primitive variable holds the value *directly* (the bits of the number), not a reference to an object. It lives on the stack or inline in an object, and has no methods. **Reference type (object type):** Everything else — classes, interfaces, arrays. A variable of a reference type holds a *reference* (a pointer) to an object on the heap. **Wrapper class:** For each primitive there is a matching class that *wraps* a single primitive value as an object: `int`→`Integer`, `double`→`Double`, `boolean`→`Boolean`, `char`→`Character`, `long`→`Long`, etc. An `Integer` is a real object on the heap whose only job is to carry one `int`. **Generics:** A way to parameterize a type, e.g. `List<Integer>` is "a list of Integers". The thing in the angle brackets is the **type argument**. ## The rule **A generic type argument must be a reference type. Primitives are not allowed.** So: ```java List<int> nums; // COMPILE ERROR: unexpected type, required reference List<Integer> nums; // OK ``` This applies to *every* primitive and *every* generic position: `Map<int, String>`, `Optional<double>`, `Comparable<boolean>` are all illegal. ## Why — type erasure Java generics are implemented by **type erasure**: the generic type information is used by the compiler for type-checking, then *erased* in the bytecode. A `List<Integer>` and a `List<String>` are both just `List` at runtime, and internally the list stores its elements as `Object`. A primitive `int` is not an `Object` and cannot be stored in an `Object` slot — only a reference can. So the language simply forbids primitive type arguments rather than letting you write code that couldn't be represented. ## What you do instead — autoboxing You use the wrapper type and let **autoboxing/unboxing** bridge the gap: ```java List<Integer> nums = new ArrayList<>(); nums.add(5); // autobox: int 5 -> Integer.valueOf(5) int first = nums.get(0); // auto-unbox: Integer -> intValue() ``` *Autoboxing* is the compiler automatically inserting `Integer.valueOf(...)` where an `int` is used but an `Integer` is expected. *Unboxing* is the reverse, inserting `.intValue()`. ## The costs 1. **Memory:** each `Integer` is a separate heap object (header + the int + alignment ≈ 16 bytes) plus the reference to it, versus 4 bytes for a raw `int`. A `List<Integer>` of a million numbers uses far more memory than an `int[]`. 2. **CPU / allocation:** boxing in a tight loop creates garbage and adds indirection (you follow a pointer to read the value), hurting cache locality. In hot paths this is measurable. 3. **NPE risk:** an `Integer` can be `null`. Unboxing a `null` throws `NullPointerException` — `int x = someInteger;` blows up if `someInteger` is null. ## When it matters / alternatives For ordinary code, `List<Integer>` is fine. For large primitive collections or hot loops, use a primitive array (`int[]`), or a primitive-specialized collection library (Eclipse Collections, fastutil, Trove, HPPC) that stores `int` directly with no boxing. The JDK also offers primitive streams (`IntStream`, `LongStream`, `DoubleStream`) to avoid boxing in stream pipelines. With all this, a learner at any level can answer: *List<int> is illegal because generics require reference types (a consequence of erasure); use List<Integer> with autoboxing, and be aware of the memory/CPU/NPE costs.*

  • What happens when you call nums.add(5) on a List<Integer>?
    The compiler autoboxes the int 5 into an Integer via Integer.valueOf(5) and stores that object reference in the list.
  • Why does the language forbid List<int> rather than just supporting it?
    Because of type erasure: at runtime the list stores elements as Object, and a primitive int isn't an Object. Project Valhalla aims to add primitive/value-type generics in the future.

A generic container is a row of mailboxes that each hold an envelope (a reference). A primitive int is a loose coin — it doesn't fit in a mailbox until you put it in an envelope (the Integer wrapper).

saying these in an interview costs you the question

  • Claiming List<int> works (it does not compile)
  • Saying Integer and int are the same thing with no overhead
  • Forgetting that unboxing a null Integer throws NullPointerException

context

open as a page

Why can't a generic class declare a static field whose type is its own type parameter, e.g. `static T value;` in `class Box<T>`?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Because a type parameter like T belongs to each object separately, but a static field is shared by the whole class. The class can't agree on one T for all objects, so Java forbids it.

open as a page

Why does declaring both method(List<String>) and method(List<Integer>) in the same class fail to compile?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Java removes the generic part at compile time, so both methods become method(List). That leaves two methods with the same name and same parameter types, which is not allowed.

open as a page

Why can't you write `new T()` inside a generic class or method in Java, and what is the standard workaround?

level: middleimportance: must knowfreq 70%

basics

~20 s

Java erases the generic type T at compile time, so at runtime the program doesn't know which class T is and can't call its constructor. The usual fix is to pass a Class<T> object and call clazz.getDeclaredConstructor().newInstance(), or pass a factory/supplier that creates the object for you.

open as a page

Why does Java reject `obj instanceof List<String>` at compile time, and what is the only generic form of `instanceof` it does allow?

level: middleimportance: must knowfreq 62%

basics

~20 s

Java erases generic type info at compile time, so at runtime there is no String type stored inside the list to check against. You can only write obj instanceof List<?> (the unbounded wildcard) or obj instanceof List (raw).

open as a page

What correctness bugs arise from boxing when you use wrapper types like Integer in generic collections?

level: middleimportance: must knowfreq 50%

basics

~20 s

Two big bugs. First, an Integer can be null, and unboxing a null throws NullPointerException. Second, comparing boxed numbers with == compares object identity, not value, so it can give wrong results; use equals() or compare as primitives.

open as a page

Why does Java reject `new T[]` and `new List<String>[]`, and how do you create such arrays safely?

level: seniorimportance: must knowfreq 62%

basics

~20 s

Arrays in Java remember their element type at runtime and check every store against it, but generic types are erased and lost at runtime. Allowing generic arrays would let bad values slip in undetected, so the compiler forbids new T[] and new List<String>[]. The common fix is to create an Object[] (or use Array.newInstance with a Class token) and cast.

open as a page

When creating instances of a type parameter, when should you use a `Supplier<T>`/factory versus a `Class<T>` token?

level: juniorimportance: should knowfreq 35%

basics

~20 s

Use a Supplier<T> (or factory) when you just need to make new objects — it's simple, checked by the compiler, works with any constructor, and uses no reflection. Use a Class<T> token when you also need the actual class for other reasons, like reflection, casting, or building a real typed array.

open as a page

What is a type token in Java, and why is one needed given how generics work at runtime?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A type token is a Class object (like String.class) you pass into a method so the code knows the type at runtime. It's needed because Java generics forget their type at runtime, so passing the Class restores that information.

open as a page

What is a `Class<T>` type token, and how does it let generic code work around erasure?

level: middleimportance: should knowfreq 48%

basics

~20 s

A type token is just a Class<T> object (like String.class) that you pass into generic code. Because the Class object still knows the concrete type at runtime, the code can use it to create instances, cast, or build arrays even though the type parameter T itself was erased.

open as a page

Why can't a generic class in Java extend Throwable (e.g. why is `class MyException<T> extends Exception` illegal)?

level: middleimportance: should knowfreq 35%

basics

~20 s

Java forbids it. A type that can be thrown and caught must be a real, fixed type at runtime, but generics are erased and lose their type info, so the compiler blocks any generic subclass of Throwable.

open as a page

Given the instanceof restriction, how would you determine the element type of a `List` at runtime, and what are the limits of any approach?

level: middleimportance: should knowfreq 30%

basics

~20 s

You can't ask the list its element type — that was erased. Test it with instanceof List<?>, then look at an actual element (list.get(0) instanceof String). This fails for an empty list, where the element type is simply unknowable at runtime.

open as a page

What are the performance implications of using List<Integer> instead of int[], and when does it matter?

level: middleimportance: should knowfreq 55%

basics

~20 s

Each number in a List<Integer> becomes a separate object on the heap with extra memory, plus the cost of boxing and unboxing. An int[] stores the raw numbers compactly with no boxing. It matters most for large data or tight loops.

open as a page

A static method needs to operate on a type parameter, but it can't use the enclosing class's T. How do you write it correctly, and what is different about that type parameter?

level: middleimportance: should knowfreq 45%

basics

~10 s

Give the static method its own type parameter, like static <U> U pick(U x). That U is decided each time you call the method, not per object, so it doesn't clash with the rule.

open as a page

You have two pieces of logic that conceptually take a List<String> and a List<Integer>. How do you expose both given the erasure clash?

level: middleimportance: should knowfreq 34%

basics

~10 s

Give the methods different names, or make them differ in their raw parameter types, or add an extra parameter. You cannot keep the same name with only the type argument different.

open as a page

What does Class.cast(Object) do, and how does it differ from a normal `(T)` cast for recovering an erased type?

level: middleimportance: should knowfreq 40%

basics

~20 s

Class.cast checks at runtime that the object really is of the token's type and returns it typed, throwing ClassCastException right away if not. A plain (T) cast is erased, so it doesn't actually check T and the failure can surface later somewhere else.

open as a page

Why can't you write a `catch (T e)` clause where T is a type parameter, and what does the compiler say?

level: seniorimportance: should knowfreq 30%

basics

~20 s

You can't catch a type variable. A catch clause needs a concrete exception type the JVM can test at runtime, but T is erased and not known at runtime, so the compiler rejects catch (T e).

open as a page

What does it mean for a type to be 'reifiable' in Java, and which generic types are reifiable?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A reifiable type is one whose full type information is still available at runtime. Plain types, raw types, and unbounded wildcards like List<?> are reifiable; parameterized types like List<String> are not, because erasure throws the type argument away.

open as a page

Inside a generic class `Box<T>`, why can't you write `x instanceof T` (or `new T()`), and what is the standard workaround?

level: seniorimportance: should knowfreq 38%

basics

~20 s

At runtime T is erased to Object (or its bound), so there is no real T to test against — x instanceof T and new T() are both illegal. The fix is to pass a Class<T> object and use clazz.isInstance(x) and clazz.getDeclaredConstructor().newInstance().

open as a page

Java arrays support primitives (int[]) but generics do not (no List<int>). Why the asymmetry?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Arrays are a built-in language feature that knew about primitives from day one and the JVM has separate bytecode for primitive arrays. Generics were added later and use type erasure, where elements are treated as Object, which can't hold a primitive. So arrays got primitives, generics didn't.

open as a page

Explain the JLS rule that governs the 'same erasure' overload clash and what 'override-equivalent' means.

level: seniorimportance: should knowfreq 38%

basics

~10 s

Java forbids two methods in a class whose names and erased parameter types match. After erasure the generic parts are gone, so such methods would be indistinguishable, which is the rule the compiler enforces.

open as a page

What is a super type token (the TypeReference idiom), and how does subclassing let it capture a full parameterized type like List<String> despite erasure?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A super type token captures a full generic type like List<String> by creating an anonymous subclass that fixes the type argument. Java writes that argument into the class's Signature metadata, and the token reads it back at runtime with getGenericSuperclass — something a plain Class can't do.

open as a page

Why did Java implement generics with type erasure rather than reified generics, and what restrictions does that choice impose?

level: principalimportance: should knowfreq 40%

basics

~20 s

Java chose erasure so that generic code stays fully compatible with older non-generic code and runs on the existing JVM without changing the bytecode format. The price is that type arguments vanish at runtime, which is why you can't do new T(), new T[], obj instanceof T, or overload methods that differ only by their type argument.

open as a page

How do you write a generic method that calls code which may throw various exceptions, given you can't catch a type parameter — and how does multi-catch fit in?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Catch a concrete supertype (like Exception) instead of a type parameter, or list the specific exception types with multi-catch (catch (IOException | SQLException e)). You can also declare throws T and let the caller handle it.

open as a page

A developer wants 'a separate static counter for each parameterization of `Counter<T>`' so `Counter<String>` and `Counter<Integer>` count independently. Why doesn't a static field in `Counter<T>` give them this, and what should they do instead?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

A static field is shared by all instances of the class, and Java erases the type argument, so Counter<String> and Counter<Integer> use the very same static counter — not separate ones. To count per type, key a map by the Class object, e.g. a Map<Class<?>, Integer>.

open as a page

These exception restrictions are a consequence of type erasure. How would they change in a language with reified generics, and what trade-off did Java make?

level: principalimportance: nice to knowfreq 12%

basics

~20 s

The restrictions exist because Java erases generic types at runtime, so the JVM can't tell parameterized exceptions apart. A language that keeps (reifies) generic types at runtime could allow generic exceptions and catching type variables — but Java chose erasure to stay backward compatible.

open as a page

How does Java's 'no instanceof with parameterized types' restriction compare to C#, where `obj is List<string>` works — and why did Java make this design choice?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

C# reifies generics: the runtime keeps the type argument, so obj is List<string> works. Java erases generics, so the type argument is gone at runtime and the check is impossible. Java chose erasure to stay backward-compatible with pre-generics code and bytecode.

open as a page

How would you design a high-throughput numeric component in Java to avoid the boxing forced by generics?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Avoid generic collections of wrappers. Use primitive arrays (int[], long[]) or primitive streams (IntStream) for the hot path, and reach for primitive-collection libraries like fastutil or Eclipse Collections when you need growable maps/lists of primitives without boxing.

open as a page

From a language-design standpoint, why did Java's designers end up forbidding static members of a class's type parameter, and what trade-off does this reflect compared with reified-generic languages?

level: principalimportance: nice to knowfreq 15%

basics

~20 s

Java chose to add generics by erasing type arguments so old (pre-generics) code and bytecode kept working. Erasure means one runtime class per generic class, so there's no per-type static storage — making static members of T impossible. Reified languages like C# kept per-type runtime types, so they can have such statics.

open as a page

showing 1–30 of 32