Skip to content

Generics Basics Interview Questions & Answers

22 questions Updated 2026-06-20 Share:

Java generics interview questions — what generics are and why, generic classes and methods, type parameters, the diamond operator, type safety, raw types, and generic constructors.

Read the in-depth guideJava Generics — Type Safety, Generic Classes, Methods & the Diamond Operator(opens in new tab)
22 of 22

Generics let you parameterize a class, interface, or method by type, so the same code works for many types while the compiler still enforces type safety. The two big wins are compile-time type checking (a wrong type is a build error, not a runtime crash) and no manual casts when you read values back out.

List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);            // compile error — caught early
String n = names.get(0);  // no cast needed; compiler knows it's a String

Before generics (Java 5), collections held raw Object, so you cast on every get and a wrong type blew up at runtime with ClassCastException. Generics move that failure to compile time.

You add a type parameter in angle brackets after the class name; that parameter then stands in for a real type everywhere inside the class. Callers supply the actual type when they create an instance.

class Box<T> {            // T is the type parameter
  private T value;
  public void set(T v) { value = v; }
  public T get() { return value; }
}

Box<String> b = new Box<>();
b.set("hi");
String s = b.get();       // returns T = String, no cast

The type argument (String) replaces T for that instance, so the compiler rejects b.set(42) and lets get() return a String directly.

Type parameters are single uppercase letters by convention, each hinting at a role:

Letter Meaning
T Type (the generic default)
E Element (used by collections)
K Key (maps)
V Value (maps)
N Number
R Return type (functional interfaces)
S, U second/third types when more are needed
interface Map<K, V> { V get(K key); }   // K = key, V = value

These are pure convention — the compiler accepts any valid identifier — but following them makes generic signatures instantly readable to other developers.

A generic method declares its own type parameter, written before the return type. It can appear in any class (generic or not), and the type is scoped to that single method.

// <T> introduces the type parameter for this method only
static <T> T firstOrNull(List<T> list) {
  return list.isEmpty() ? null : list.get(0);
}

String s = firstOrNull(List.of("a", "b")); // T inferred as String
Integer n = firstOrNull(List.<Integer>of()); // T = Integer

The <T> between the modifiers and the return type is what makes it generic — without it, T would be an undefined symbol. The method's type parameter is independent of any type parameter on the enclosing class.

The compiler performs type inference: it looks at the argument types and the target type (what the result is assigned to) to deduce the type parameter, so you rarely write it explicitly.

static <T> List<T> singleton(T item) { return List.of(item); }

var xs = singleton("hi");      // T inferred from "hi" -> String
List<Integer> ys = singleton(7); // T inferred -> Integer

When inference can't decide (or you want to override it), supply an explicit type witness before the method name: Collections.<String>emptyList(). This is occasionally needed when there are no arguments to infer from.

The diamond operator <> (Java 7+) lets you omit the type arguments on the right side of an assignment; the compiler infers them from the declared type on the left (the target type).

// verbose, pre-Java 7
Map<String, List<Integer>> m = new HashMap<String, List<Integer>>();

// diamond — compiler infers <String, List<Integer>>
Map<String, List<Integer>> m2 = new HashMap<>();

It cuts redundant boilerplate without losing any type safety — new HashMap<>() is still fully generic, just inferred. Note new HashMap() (no diamond) is a raw type and is not the same thing.

A generic interface declares type parameters just like a class. An implementor either fixes the type or stays generic by passing its own parameter through.

interface Repository<T> {
  void save(T item);
  T findById(int id);
}

// fix the type
class UserRepo implements Repository<User> {
  public void save(User u) { }
  public User findById(int id) { return null; }
}

// stay generic
class MemRepo<T> implements Repository<T> { /* ... */ }

Core JDK interfaces like Comparable<T>, Iterable<E>, and Comparator<T> work this way, which is why class Money implements Comparable<Money> gives you a type-safe compareTo(Money).

Yes — list them comma-separated inside the angle brackets. The classic example is Map<K, V>, which is parameterized by both a key type and a value type.

class Pair<A, B> {
  final A first;
  final B second;
  Pair(A a, B b) { first = a; second = b; }
}

Pair<String, Integer> p = new Pair<>("age", 30);
String key = p.first;   // A = String
int val = p.second;     // B = Integer

Each parameter is independent, so Pair<String, Integer> and Pair<Integer, String> are distinct, incompatible types.

Without generics you'd store everything as Object, deferring all type checking to runtime casts that can fail. Generics let the compiler verify types, turning a potential ClassCastException into a build error.

// pre-generics style
List raw = new ArrayList();
raw.add("hi");
Integer n = (Integer) raw.get(0);  // compiles, ClassCastException at runtime

// generic style
List<String> safe = new ArrayList<>();
safe.add("hi");
Integer m = safe.get(0);           // compile error — caught immediately

The cast disappears because the compiler already knows the element type, and it inserts any needed cast for you behind the scenes.

A raw type is a generic type used without its type argument, e.g. List instead of List<String>. It exists only for backward compatibility with pre-Java-5 code, and using it opts out of type safety — the compiler can no longer check element types.

List raw = new ArrayList();   // raw type
raw.add("hi");
raw.add(42);                  // no complaint — types unchecked
String s = (String) raw.get(1); // ClassCastException at runtime

Raw types produce unchecked warnings and re-introduce exactly the runtime failures generics were designed to prevent. Always parameterize — use List<Object> if you genuinely want any type, which keeps checking on.

They look similar but behave very differently:

Type Meaning Type-checked?
List raw — legacy escape hatch No (unchecked warnings)
List<Object> a list that can hold any type Yes
List<?> a list of some unknown type Yes (read-only)
List<String> strings = new ArrayList<>();
List<Object> objs = strings; // compile error — not the same type
List<?> any = strings;       // OK — wildcard accepts any List<X>
any.add("x");                // error — can't add to List<?> (type unknown)

The key trap: List<Object> is not a supertype of List<String> (generics are invariant), so you can't assign one to the other — that's where wildcards come in.

Because of type erasure, the type parameter T doesn't exist at runtime — the JVM has no idea what concrete class to instantiate, so new T() is a compile error. There's no constructor to call when T is unknown.

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

The standard workaround is to pass a factory or a Class<T> token and use reflection:

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

(Erasure has its own page — here it's enough to know T isn't a real runtime type.)

Arrays are reified — they know and check their element type at runtime — while generics are erased. Mixing the two would let an unchecked store slip through, so the compiler forbids new T[] and new List<String>[].

class Stack<T> {
  // T[] data = new T[10];        // compile error — generic array creation
  Object[] data = new Object[10];  // workaround: store as Object[]
  @SuppressWarnings("unchecked")
  T get(int i) { return (T) data[i]; } // cast on read
}

The common pattern is an Object[] backing store with a suppressed cast on read, which is exactly how ArrayList is implemented internally.

Generics are invariant: even though String is a subtype of Object, List<String> is not a subtype of List<Object>. If it were, you could break type safety by adding the wrong type through the supertype reference.

List<String> strings = new ArrayList<>();
List<Object> objs = strings;   // suppose this were allowed...
objs.add(42);                  // ...then this would put an Integer in
String s = strings.get(0);     // ...and this would explode at runtime

The compiler blocks the assignment to keep that from ever happening. Arrays, by contrast, are covariant (String[] is an Object[]), which is exactly why array stores are checked at runtime and can throw ArrayStoreException.

A bound restricts what types a parameter accepts using extends. <T extends Number> means "any T that is Number or a subclass," which lets you call that bound's methods on T.

// T must be a Number, so .doubleValue() is available
static <T extends Number> double sum(List<T> nums) {
  double total = 0;
  for (T n : nums) total += n.doubleValue();
  return total;
}

sum(List.of(1, 2, 3));       // OK — Integer extends Number
sum(List.of("a", "b"));      // compile error — String isn't a Number

extends works for both classes and interfaces here, and you can require several bounds with & (<T extends Number & Comparable<T>>). Wildcards and more advanced bounding are covered on the wildcards page.

@SuppressWarnings("unchecked") silences the compiler's unchecked warnings — the ones emitted when you do a cast or operation the compiler can't fully verify because of erasure. Use it only when you've reasoned that the cast is safe.

@SuppressWarnings("unchecked")           // safe: we only ever store T here
T get(int i) { return (T) data[i]; }

Best practice: put it on the narrowest scope possible (a single local variable or method, never a whole class), and add a comment explaining why the operation is actually safe. Suppressing warnings blindly hides real bugs.

var infers the full generic type from the initializer, so you keep complete type safety without repeating long parameter lists. But the initializer must carry the type information — a bare diamond gives var nothing to infer.

var users = new HashMap<String, List<User>>(); // inferred fully
var first = users.get("a");                     // List<User>

var bad = new ArrayList<>();   // infers ArrayList<Object> — probably not wanted

With var the right-hand side must specify the type arguments (the diamond can't borrow them from a left-hand declaration that no longer exists). So write new ArrayList<String>(), not new ArrayList<>(), when using var.

Yes — a constructor can declare type parameters independent of the class's own, written before the constructor name. This is rare but valid when the constructor needs a type only for its arguments.

class Holder<T> {
  private T value;
  // S is the constructor's own parameter, separate from class T
  <S extends T> Holder(S initial) {
    this.value = initial;
  }
}

Here S is scoped to the constructor; the class is still Holder<T>. Most code just reuses the class's T, but a generic constructor lets you accept a more specific or unrelated type for construction only.

A static method belongs to the class, not an instance, so it can't see the class's type parameter — that parameter only exists per object. A static method that needs generics must declare its own.

class Box<T> {
  T value;
  // static <T> needed — the class's T is not in scope here
  static <U> Box<U> of(U v) {
    Box<U> b = new Box<>();
    b.value = v;
    return b;
  }
}

Box<String> b = Box.of("hi");   // U inferred as String

Using the class's T in a static method is a compile error precisely because there's no instance to bind it to. (Naming the static parameter the same letter as the class's is legal but confusing — use a different letter.)

Java generics are a compile-time feature implemented by type erasure: the compiler checks types and inserts casts, then removes the type parameters so the bytecode works on Object (or the bound). At runtime, a List<String> and a List<Integer> are just List.

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

The benefit is backward compatibility with pre-generics bytecode; the cost is that types aren't available for reflection or runtime checks. The full mechanics and consequences live on the type-erasure page.

Because of erasure, type parameters become Object at runtime, and primitives aren't objects — so List<int> is illegal. You must use the wrapper class (Integer, Double, …), and autoboxing bridges the gap.

List<int> nums;            // compile error
List<Integer> nums2 = new ArrayList<>();
nums2.add(5);              // autoboxed int -> Integer
int x = nums2.get(0);      // unboxed Integer -> int

The trade-off is boxing overhead for large numeric collections, which is why specialized primitive streams (IntStream) and libraries with primitive collections exist. Project Valhalla aims to relax this in the future.

Comparable<T> is generic so compareTo takes a typed argument instead of raw Object — no cast, and you can't accidentally compare against the wrong type. You parameterize it with your own class.

class Version implements Comparable<Version> {
  int number;
  // typed parameter — no casting from Object
  public int compareTo(Version other) {
    return Integer.compare(this.number, other.number);
  }
}

Compare this to the legacy raw Comparable, where compareTo(Object o) forced a downcast. Bounding on Comparable (<T extends Comparable<T>>) is also how methods like Collections.max guarantee their elements are orderable.

More ways to practice

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

or
Join our WhatsApp Channel