Skip to content

Type Erasure Interview Questions & Answers

21 questions Updated 2026-06-20 Share:

Java type erasure interview questions — how the compiler erases generics, bridge methods, reifiable vs non-reifiable types, generics and arrays, restrictions on generics, and runtime type information.

Read the in-depth guideJava Type Erasure Explained — How Generics Vanish at Runtime(opens in new tab)
21 of 21

Type erasure is the process by which the compiler removes all generic type information during compilation. Generics exist only at compile time for type checking; at runtime the type parameters are gone, so a List<String> and a List<Integer> are both just plain List.

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true — both ArrayList

The compiler replaces type parameters with their bound (or Object if unbounded) and inserts casts where needed. The result is bytecode that looks essentially like pre-generics Java. Rule of thumb: generics are a compile-time fiction — nothing about the type argument survives to runtime.

The driving reason was backward compatibility. Generics arrived in Java 5, long after millions of lines of pre-generics code existed. Erasure let generic code and legacy raw-type code interoperate on the same JVM without changing the bytecode format or breaking existing libraries.

List<String> generic = new ArrayList<>();
List raw = generic;          // legacy raw type — still works
raw.add("ok");               // unchecked, but compiles and runs

A pre-generics .class file and a generics-aware one produce compatible bytecode, and a List from old code can flow into new generic code. Rule of thumb: erasure was the price of adding generics without forking the platform — runtime reification (like C#) would have broken the existing ecosystem.

The compiler applies three transformations: it replaces type parameters with their leftmost bound (or Object if unbounded), inserts casts at every site where erased values are read out, and generates bridge methods to preserve polymorphism. The runtime never sees T.

// You write:
class Box<T> { T value; T get() { return value; } }
String s = new Box<String>().get();

// After erasure (conceptually):
class Box { Object value; Object get() { return value; } }
String s = (String) new Box().get();   // compiler inserts the cast

A bounded <T extends Number> erases to Number, not Object. Rule of thumb: type parameter -> bound, plus compiler-inserted casts — the casts are guaranteed safe because the compiler already type-checked the code.

A type parameter erases to its leftmost (first) bound, not always Object. An unbounded <T> erases to Object, but <T extends Number> erases to Number, which lets the compiler call Number methods directly on T.

<T extends Number> double sum(T a, T b) {
  return a.doubleValue() + b.doubleValue(); // legal — T erases to Number
}
// erased signature: double sum(Number a, Number b)

<T extends Comparable<T> & Serializable> T pick(T x) { return x; }
// erases to Comparable (the FIRST bound)

With multiple bounds (A & B), only the first drives erasure; that's why you put the class or most-used interface first. Rule of thumb: the erased type is the first bound — choose bound order so the useful methods survive.

A reifiable type is one whose type information is fully available at runtime — its representation is complete after erasure. A non-reifiable type loses information to erasure, so the JVM can't fully reconstruct it.

Reifiable (survives) Non-reifiable (erased)
String, Integer List<String>
List, Map (raw) List<? extends Number>
List<?> (unbounded wildcard) T (type parameter)
int[], String[] List<String>[]
o instanceof List<?>      // legal — unbounded wildcard is reifiable
o instanceof List<String> // ERROR — non-reifiable, info was erased

Rule of thumb: if a type still carries a generic argument other than ?, it's non-reifiable and you can't use it with instanceof, array creation, or .class.

Because there is only one Class object for List, shared by every parameterization. After erasure List<String>, List<Integer>, and raw List are the same class, so a List<String>.class literal would be meaningless — there's nothing distinct to point to.

Class<?> c = List.class;          // legal — the one List class
Class<?> d = List<String>.class;  // compile error
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true

getClass() on any generic instance returns the raw class object for the same reason. Rule of thumb: there's one Class per erased type, so a parameterized class literal can't exist.

Because at runtime T has been erased — the JVM has no idea which class to instantiate, and it can't guarantee T even has a no-arg constructor. The new bytecode needs a concrete class, which erasure has removed.

class Factory<T> {
  T make() { return new T(); }   // compile error — cannot instantiate T
}

// Workaround: pass a Class token or a supplier
class Factory<T> {
  T make(Class<T> type) throws Exception {
    return type.getDeclaredConstructor().newInstance(); // reflection
  }
  T make(Supplier<T> s) { return s.get(); }             // cleaner
}

Rule of thumb: to create instances of a type parameter, pass in a Class<T> token or a Supplier<T> — the type must come from somewhere reifiable.

Arrays are reified — they remember and enforce their element type at runtime (storing a wrong type throws ArrayStoreException). Generics are erased. The two models are incompatible: a generic array couldn't perform its runtime store-check because the element type was erased.

T[] a = new T[10];               // compile error — generic array creation
List<String>[] b = new List<String>[10]; // compile error

// Workarounds:
List<String>[] c = (List<String>[]) new List[10]; // unchecked cast
T[] d = (T[]) Array.newInstance(componentType, 10); // reflection + Class token

Rule of thumb: you can't directly create arrays of a non-reifiable type; use a List<List<String>> or an unchecked cast from a raw array.

Heap pollution occurs when a variable of a parameterized type points to an object that is not of that type — usually because erasure let an incompatible value slip in. Generic arrays are banned precisely because they'd make this trivially easy and undetectable.

List<String>[] arr = (List<String>[]) new List[1]; // unchecked
Object[] objs = arr;                  // arrays are covariant
objs[0] = List.of(42);                // stores List<Integer> — no error!
String s = arr[0].get(0);             // ClassCastException at READ time

The corruption is silent until a later read fails far from the cause. Rule of thumb: heap pollution = a generic variable holding the wrong element type; it surfaces as a surprise ClassCastException from compiler-inserted casts.

instanceof is a runtime check, but the generic type argument was erased, so there's nothing to test against. The JVM could only tell you it's a List, never a List<String>. Only reifiable types (raw types or the unbounded wildcard) are allowed.

if (obj instanceof List<String>) { } // compile error
if (obj instanceof List<?>) { }       // OK — unbounded wildcard is reifiable
if (obj instanceof List) { }          // OK — raw type

@SuppressWarnings("unchecked")
List<String> l = (List<String>) obj;  // cast compiles but is unchecked

Rule of thumb: check against List<?> or the raw List; you can never instanceof a specific type argument because it doesn't exist at runtime.

A bridge method is a synthetic method the compiler generates to preserve polymorphism after erasure. When a generic supertype's method erases to a different signature than the subtype's override, the compiler adds a bridge so dynamic dispatch still finds the real method.

class Node<T> { void set(T t) { } }            // erases to set(Object)
class StringNode extends Node<String> {
  @Override void set(String s) { }             // set(String)
}
// Compiler adds a synthetic bridge in StringNode:
// void set(Object o) { set((String) o); }      // forwards to the real method

Without the bridge, Node<String> n = new StringNode(); n.set("x") would call the inherited set(Object) and skip the override. Rule of thumb: bridge methods are invisible glue that keep overriding working across erased generic signatures.

Bridge methods are marked synthetic and carry the ACC_BRIDGE flag in the bytecode. You can detect them at runtime via reflection with Method.isBridge() and Method.isSynthetic(). They never appear in your source and you shouldn't call them directly.

for (Method m : StringNode.class.getDeclaredMethods()) {
  System.out.println(m + " bridge=" + m.isBridge());
}
// set(String) bridge=false
// set(Object) bridge=true   <- the synthetic bridge

They're why reflective scans sometimes find "duplicate" methods that differ only by parameter type. Rule of thumb: filter out isBridge()/ isSynthetic() methods when introspecting generic classes.

Because after erasure they have the same signature, and a class can't have two methods with identical signatures. The compiler reports a "name clash" even though the source-level parameter types look different.

class Printer {
  void print(List<String> s) { }   // erases to print(List)
  void print(List<Integer> i) { }  // also erases to print(List) — CLASH!
}
// error: name clash: both methods have the same erasure

The fix is to give them genuinely different erased signatures (different raw types or arity) or rename one. Rule of thumb: overloads must differ after erasure — distinct type arguments alone are not enough.

A type token is a Class<T> object passed in to recover the type information that erasure removed. Since the type argument is gone at runtime, handing the method a Class<T> lets it instantiate, cast, or check types safely.

<T> T fromJson(String json, Class<T> type) {
  Object parsed = parse(json);
  return type.cast(parsed);          // typed cast via the token
}
User u = fromJson(s, User.class);     // T inferred from the token

<T> T create(Class<T> type) throws Exception {
  return type.getDeclaredConstructor().newInstance();
}

Frameworks like Jackson and Spring rely on this everywhere. Rule of thumb: when you need the runtime type of T, pass a Class<T> token — it's the standard escape hatch from erasure.

A plain Class<T> token can't represent a parameterized type like List<String> (no such class literal). The super type token trick (used by Jackson's TypeReference, Guava's TypeToken) captures it by subclassing an abstract generic class and reading the type argument via reflection from the generic superclass, which the compiler does record.

// The anonymous subclass embeds List<String> in its signature:
TypeReference<List<String>> ref = new TypeReference<List<String>>() {};
// Internally:
Type t = getClass().getGenericSuperclass();           // TypeReference<List<String>>
Type arg = ((ParameterizedType) t).getActualTypeArguments()[0]; // List<String>

Generic type info on class/method signatures survives erasure (in metadata), even though instance-level arguments don't. Rule of thumb: subclassing a generic type pins its arguments in the class signature, which reflection can read back.

A type parameter belongs to a specific instance's parameterization, but a static member is shared across all parameterizations — and after erasure there's only one class anyway. There's no single T for the static context to refer to, so it's forbidden.

class Box<T> {
  static T shared;              // compile error — T in static context
  static void set(T t) { }      // compile error
  static <U> U pick(U u) { return u; } // OK — its OWN type parameter
}

A static method may declare its own type parameter, which is fine. Rule of thumb: the class's T is per-instance; statics are class-wide, so they can't use it — give a static method its own type parameter instead.

A generic class cannot extend Throwable, and you cannot catch a type parameter, because catch is a runtime dispatch on the exception's actual class — which erasure has removed. You can declare a method that throws a type parameter bounded by Throwable.

class MyException<T> extends Exception { }  // compile error — generic Throwable

<T extends Throwable> void run() {
  try { /* ... */ }
  catch (T e) { }              // compile error — cannot catch type parameter
}

<T extends Throwable> void rethrow(T t) throws T { throw t; } // OK

If catch were allowed, two catch (T) clauses could erase to the same catch (Exception). Rule of thumb: you can throw but never catch a type parameter, and exception classes can't be generic.

Instance type arguments vanish, but generic info embedded in declarations is kept in the class file's signature metadata and is readable via reflection (java.lang.reflect.Type). This includes field types, method parameter/return types, type variable bounds, and the generic superclass / interfaces.

class Repo { List<String> names; }
Field f = Repo.class.getDeclaredField("names");
Type t = f.getGenericType();                 // List<String> — preserved!
ParameterizedType pt = (ParameterizedType) t;
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String

What's lost is the argument of a runtime object (new ArrayList<String>() knows nothing about String). Rule of thumb: declaration-site generics live in metadata; use-site instance generics do not.

A varargs parameter of a generic type creates a generic array under the hood (T... becomes T[]), which triggers an unavoidable unchecked / "possible heap pollution" warning. @SafeVarargs is your assertion that the method only reads from the varargs and never pollutes the array, which suppresses the warning at the declaration and all call sites.

@SafeVarargs
static <T> List<T> listOf(T... items) {   // T[] is a non-reifiable array
  return new ArrayList<>(Arrays.asList(items)); // safe — read only
}

It's applicable only to methods that can't be overridden (static, final, or private). Rule of thumb: use @SafeVarargs when your generic varargs method is read-only and provably safe from heap pollution.

An unchecked warning means the compiler can't verify the type safety of an operation because the needed type information was erased — typically a cast to a parameterized type or use of a raw type. The code compiles, but the compiler is telling you it can't guarantee the runtime cast won't fail.

Object o = new ArrayList<String>();
List<String> list = (List<String>) o; // unchecked — only List is verifiable

List raw = new ArrayList();
raw.add("x");                          // unchecked call to add(E)

Suppress only when you've manually proven safety, with the narrowest scope and a comment, via @SuppressWarnings("unchecked"). Rule of thumb: an unchecked warning is erasure admitting it can't check you — treat each one as a potential ClassCastException to justify.

With reified generics (C#/.NET), the type argument is preserved at runtime: List<int> and List<string> are distinct types, you can call typeof(T), do new T(), and use is List<string>. Java's erased generics throw all of that away after compilation.

// Java — none of these work:
T.class;  new T();  new T[10];  obj instanceof List<String>;

// C# equivalents all work because T is reified at runtime.

Reification costs a specialized type per parameterization and breaks legacy interop; erasure is cheaper and compatible but loses runtime type identity. Rule of thumb: Java traded runtime type knowledge for backward compatibility — every generics restriction you hit traces back to that choice.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel