| Type Parameter | Placeholder (e.g. T, E, K, V) replaced by real type at compile time |
| Type Erasure | JVM removes generic type info at runtime; all T become Object (or bound) |
| Upper Bound (? extends T) | Wildcard accepts T or any subtype; covariant — read-only |
| Lower Bound (? super T) | Wildcard accepts T or any supertype; contravariant — write-friendly |
| PECS | Producer Extends, Consumer Super — rule for choosing wildcard direction |
| Raw Type | Using a generic class without type args (e.g. List instead of List<String>) |
| Heap Pollution | Variable of parameterized type refers to object of different type at runtime |
| Bridge Method | Synthetic method generated by compiler to preserve polymorphism after erasure |
| Reifiable Type | Type whose info is fully available at runtime (e.g. List<?>, int[], String) |
| Diamond Operator | <> — introduced Java 7, lets compiler infer generic type on right-hand side |
List<String> list
<T extends Foo>
Inserts casts, generates bridge methods
T → Object (erased)
No generic info left
Java Generics Interview Questions & Answers
Q1. What are Java Generics and why were they introduced?
A: Java Generics, introduced in Java 5 (JDK 1.5), allow classes, interfaces, and methods to operate on types specified as parameters. Before generics, collections stored Object references, requiring explicit casts that were error-prone at runtime. Generics provide compile-time type safety, eliminate the need for explicit casting, and enable writing reusable, type-safe algorithms. The compiler can catch type mismatches early, preventing ClassCastException at runtime.
Q2. How do you declare a generic class in Java?
A: A generic class is declared by appending a type parameter list (enclosed in angle brackets) after the class name. Convention uses single uppercase letters: T (type), E (element), K (key), V (value), N (number).
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// Usage
Box<String> stringBox = new Box<>("Hello Generics");
Box<Integer> intBox = new Box<>(42);
String s = stringBox.getValue(); // no cast needed
Q3. How do you declare a generic method in Java?
A: A generic method declares its own type parameter(s) before the return type. This allows the method to be parameterized independently of the enclosing class.
public class Utils {
// Generic method — type param <T> before return type
public static <T> T identity(T item) {
return item;
}
// Multiple type params
public static <K, V> Map<K, V> mapOf(K key, V value) {
Map<K, V> map = new HashMap<>();
map.put(key, value);
return map;
}
}
// Caller — type often inferred
String result = Utils.identity("test");
Map<String, Integer> m = Utils.mapOf("age", 30);
Q4. How do you declare a generic interface?
A: A generic interface is declared the same way as a generic class. Implementing classes either provide a concrete type or propagate the type parameter.
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
List<T> findAll();
}
// Concrete implementation
public class UserRepository implements Repository<User, Long> {
public User findById(Long id) { /* ... */ return null; }
public void save(User user) { /* ... */ }
public List<User> findAll() { /* ... */ return null; }
}
// Propagating the type parameter
public class GenericRepo<T> implements Repository<T, Long> {
public T findById(Long id) { /* ... */ return null; }
public void save(T entity) { /* ... */ }
public List<T> findAll() { /* ... */ return null; }
}
Q5. What is type erasure and why does Java use it?
A: Type erasure is the process by which the Java compiler removes all generic type information during compilation, replacing type parameters with their bounds (or Object if unbounded). At runtime, no generic type information exists in the bytecode. Java chose this approach for backward compatibility — Java 5 generics had to work with pre-existing non-generic bytecode and the JVM did not require changes. The compiler inserts casts where necessary to maintain type safety at the source level.
Q6. What are the implications of type erasure?
A: Type erasure has several important implications: (1) You cannot use instanceof with parameterized types — e.g., obj instanceof List<String> is illegal; only obj instanceof List<?> or obj instanceof List is allowed. (2) You cannot create instances of type parameters — new T() is illegal. (3) You cannot create arrays of parameterized types — new List<String>[10] is illegal. (4) Overloading on type parameters alone is impossible since both methods erase to the same signature. (5) Generic exceptions have restrictions. (6) Static members cannot use the class-level type parameter.
Q7. What happens to type parameters during erasure?
A: During erasure, unbounded type parameters (T) are replaced with Object. Bounded type parameters (T extends Foo) are replaced with the leftmost bound (Foo). For multiple bounds (T extends Foo & Bar), T is replaced with Foo (the first bound). The compiler inserts checkcast bytecode instructions wherever the erased type is used in a context requiring the original type.
Q8. What is an upper bounded wildcard (? extends T)?
A: The upper bounded wildcard ? extends T means "unknown type that is T or a subtype of T." It makes a parameterized type covariant. You can read elements from such a collection as type T, but you cannot add elements (except null) because the compiler cannot verify which specific subtype is present. This is used when a method needs to read from a collection.
public static double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) { // safe to read as Number
sum += n.doubleValue();
}
return sum;
}
// Works with List<Integer>, List<Double>, List<Number>
sumOfList(List.of(1, 2, 3)); // List<Integer>
sumOfList(List.of(1.5, 2.5)); // List<Double>
Q9. What is a lower bounded wildcard (? super T)?
A: The lower bounded wildcard ? super T means "unknown type that is T or a supertype of T." It makes a parameterized type contravariant. You can add elements of type T (or subtypes) safely, but reading only gives you Object. This is used when a method needs to write into a collection.
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i); // safe — Integer is subtype of ? super Integer
}
}
List<Number> nums = new ArrayList<>();
addNumbers(nums); // valid: Number is supertype of Integer
List<Object> objs = new ArrayList<>();
addNumbers(objs); // valid: Object is supertype of Integer
Q10. What is the PECS mnemonic and how do you apply it?
A: PECS stands for Producer Extends, Consumer Super. When a parameterized type acts as a producer (you read/get from it), use ? extends T. When it acts as a consumer (you write/add to it), use ? super T. This rule, formalized by Joshua Bloch in Effective Java, helps choose the correct wildcard. For example, Collections.copy(dest, src) uses ? extends T for src (producer — you read from it) and ? super T for dest (consumer — you write into it).
Q11. What is an unbounded wildcard and when is it used?
A: The unbounded wildcard ? means "unknown type." It is used when: (1) the method only uses Object methods on the elements, or (2) the code does not depend on the type parameter at all. For example, printList(List<?> list) can print any list. It is more flexible than List<Object> because List<String> IS-A List<?> but is NOT a List<Object>.
Q12. Why is List<String> not a subtype of List<Object>?
A: Java generics are invariant by default. Even though String extends Object, List<String> is NOT a subtype of List<Object>. If it were, you could do: List<Object> list = new ArrayList<String>(); list.add(42); — violating type safety. This would cause a ClassCastException when retrieving the Integer as String. Use List<? extends Object> (or List<?>) for covariance instead.
Q13. What are raw types and why are they discouraged?
A: A raw type is a generic class or interface used without any type argument — e.g., List instead of List<String>. Raw types exist for backward compatibility with pre-Java 5 code. They are discouraged because: (1) all type safety is lost, (2) the compiler issues unchecked warnings, (3) ClassCastException can occur at runtime with no warning. You should always use parameterized types in new code.
Q14. What is the difference between List<?> and List<Object>?
A: List<Object> means a list that holds only Object references explicitly; List<String> is NOT assignable to it. List<?> means a list of some unknown type; any List<X> is assignable to it. With List<?> you cannot add elements (except null) since the type is unknown; List<Object> allows adding any Object. List<?> is used for read-only operations on heterogeneous lists.
Q15. What are multiple bounds on a type parameter?
A: A type parameter can have multiple bounds using the & operator: <T extends ClassA & InterfaceB & InterfaceC>. Rules: (1) At most one class bound, and it must appear first. (2) Remaining bounds must be interfaces. (3) During erasure, T is replaced by the first bound (ClassA). Multiple bounds are useful for requiring a type to satisfy several contracts simultaneously.
public <T extends Comparable<T> & Serializable> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// T must implement both Comparable and Serializable
// Integer satisfies both — works fine
Integer bigger = max(3, 7); // returns 7
Q16. What is a generic constructor?
A: A constructor can have its own type parameters independent of the class-level type parameters. The type parameter is declared before the constructor name (same syntax as generic methods). Generic constructors are relatively uncommon but useful in specific scenarios such as factory constructors.
public class Pair<A, B> {
private final A first;
private final B second;
// Generic constructor with its own type param <U>
public <U extends A> Pair(U first, B second) {
this.first = first;
this.second = second;
}
public A getFirst() { return first; }
public B getSecond() { return second; }
}
Q17. What is type inference and how does it work with generics?
A: Type inference is the compiler's ability to determine type arguments automatically from context, without requiring explicit specification. The compiler analyzes the types of method arguments, return type context, and target type to infer type parameters. Java 7 introduced the diamond operator (<>) for constructor type inference. Java 8 extended target type inference. Java 10 introduced var for local variable type inference.
Q18. What is the diamond operator (<>) introduced in Java 7?
A: The diamond operator <> on the right-hand side of a variable assignment instructs the compiler to infer the generic type arguments from the left-hand side declaration, eliminating redundant repetition. Before Java 7: Map<String, List<Integer>> map = new HashMap<String, List<Integer>>(). With Java 7+: Map<String, List<Integer>> map = new HashMap<>(). Anonymous class type inference with diamond was added in Java 9.
Q19. How does var (Java 10) interact with generics?
A: The var keyword allows the compiler to infer the type of a local variable from the right-hand side. With generics, var infers the full parameterized type, avoiding verbose declarations. However, var can lose type information if used with wildcard types or raw types — e.g., var list = new ArrayList<>() infers ArrayList<Object> because there is no target type to guide inference. Best practice: provide explicit type when using var with generic collections.
Q20. What are bridge methods and why does the compiler generate them?
A: Bridge methods are synthetic methods generated by the Java compiler to preserve polymorphic behavior after type erasure. When a generic class is subclassed with a concrete type, the overriding method has a more specific signature than the erased superclass method. The compiler generates a bridge method with the erased signature that delegates to the actual overriding method, ensuring proper dynamic dispatch at runtime.
| Source Code | After Erasure (compiler-generated) |
|---|---|
| class Node implements Comparable<Node> { int compareTo(Node o){...} } | int compareTo(Node o){...} + bridge: int compareTo(Object o){ return compareTo((Node)o); } |
| class StringBox extends Box<String> { void set(String v){...} } | void set(String v){...} + bridge: void set(Object v){ set((String)v); } |
Q21. Why can you not create a generic array (new T[])?
A: Generic array creation is prohibited because arrays are covariant and carry runtime type information (reifiable), while generics are invariant and type-erased. If new T[] were allowed, at runtime T is erased to Object, so the actual array would be Object[]. If you then stored it as String[], a subsequent ArrayStoreException-protected assignment of a non-String would silently corrupt the array. The combination of array covariance and type erasure would break the type system. Instead, use Object[] internally (with suppressed warnings) or use ArrayList<T>.
Q22. What are reifiable and non-reifiable types?
A: A reifiable type is one whose complete type information is available at runtime. Examples: primitives (int), raw types (List), unbounded wildcards (List<?>), arrays of reifiable types (String[]), and non-generic classes (String, Integer). A non-reifiable type has type information erased at runtime. Examples: List<String>, Map<K,V>, T. You can use instanceof only with reifiable types. Generic array creation is only legal with reifiable component types.
Q23. What is heap pollution?
A: Heap pollution occurs when a variable of a parameterized type refers to an object that is not of that parameterized type at runtime. It happens when raw types or unchecked operations are mixed with generic code. The pollution is invisible until the polluted variable is actually used, at which point a ClassCastException may be thrown at an apparently unrelated location.
// Heap pollution example
List rawList = new ArrayList();
rawList.add("hello");
rawList.add(42); // raw — no compile error
List<String> strings = rawList; // unchecked warning
// Heap is now polluted — strings actually holds an Integer
String s = strings.get(1); // ClassCastException at runtime!
Q24. What is @SafeVarargs and when should you use it?
A: @SafeVarargs is an annotation (Java 7+) that suppresses "unchecked or unsafe operations" warnings related to varargs methods with generic types. When a method takes a varargs parameter of a generic type (T... args), the compiler creates an array of that type — but generic arrays are not allowed, causing a warning. @SafeVarargs tells the compiler (and callers) that the method body does not perform unsafe operations on the varargs array. It can only be applied to static methods, final instance methods, or constructors (Java 9 also allows private methods). You must guarantee the method body does not expose the array reference outside the method.
@SafeVarargs
public static <T> List<T> listOf(T... elements) {
return Arrays.asList(elements);
// Safe: we only read from elements, do not store or expose it
}
// Without @SafeVarargs — compiler warns callers about heap pollution
// With @SafeVarargs — warning suppressed at declaration and call sites
Q25. What is the difference between a type parameter and a wildcard?
A: A type parameter (T) is declared on the method/class and can be referenced multiple times to enforce relationships between types — e.g., <T> void copy(List<T> src, List<T> dest) ensures src and dest hold the same type. A wildcard (?) represents an unknown type and cannot be referenced by name; it is used for flexibility — e.g., void print(List<?> list). Use type parameters when you need to relate types; use wildcards when you just need flexibility without relating.
| Aspect | Type Parameter <T> | Wildcard <?> |
|---|---|---|
| Can be referenced? | Yes — use T as a type | No — unknown, unnamed |
| Enforce type relationship? | Yes — <T> f(List<T>, T) | No |
| Variance | Invariant by default | Covariant (extends) or contravariant (super) |
| Typical use | Return type or cross-param constraints | Method argument — read or write flexibility |
Q26. Can you overload methods that differ only in generic type parameters?
A: No. Due to type erasure, two methods that differ only in their generic type parameters erase to the same signature after compilation, causing a compile-time error. For example, void process(List<String> list) and void process(List<Integer> list) both erase to void process(List list) — the compiler rejects this as a duplicate method.
Q27. Can a generic class extend or implement a parameterized type?
A: Yes. A generic class can extend another generic class, extend a concrete parameterized type, or implement parameterized interfaces. Examples: class ArrayList<E> extends AbstractList<E> (propagates parameter), class StringList extends ArrayList<String> (fixes parameter), class HashMap<K,V> implements Map<K,V> (implements generic interface).
Q28. What is wildcard capture?
A: Wildcard capture is a mechanism by which the compiler "captures" the type represented by a wildcard into a fresh type variable in a private helper method, allowing type-safe operations on wildcard types that would otherwise be impossible. The compiler performs capture conversion automatically; you can also use a private helper with a named type parameter to work around limitations.
// Cannot directly swap in a List<?> — compiler does not know the type
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j); // delegate to capture helper
}
// Helper captures ? as T — now type-safe
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
Q29. What restrictions apply to generic exceptions?
A: Java imposes several restrictions on generic exceptions: (1) A class that extends Throwable cannot be generic — e.g., class MyException<T> extends Exception is a compile error. (2) You cannot catch a parameterized type — catch(MyException<String> e) is illegal (would need reifiable type). (3) You CAN throw a generic type T if T is bounded by Throwable — useful in sneaky throw patterns. (4) A method can declare throws T where <T extends Exception> which allows checked exception erasure tricks.
Q30. Why can static members not use the class-level type parameter?
A: Static members belong to the class itself, not to any instance. Since a generic class like Box<T> can be instantiated as Box<String> or Box<Integer>, there is only one shared static context for all parameterizations. Allowing static T would be ambiguous — which T should the static field hold? Therefore, static fields, static methods, and static nested classes cannot reference the class-level type parameter T. Static methods can declare their own separate type parameters.
Q31. What is the generic singleton factory pattern?
A: The generic singleton factory pattern uses a single instance object that can be reused across all type parameterizations, since the object's behavior does not depend on T at runtime (due to erasure). It avoids creating multiple instances for each type. The Collections.emptyList(), Collections.emptySet(), and Function.identity() methods use this pattern.
// Generic singleton factory — identity function example
public class Functions {
@SuppressWarnings("unchecked")
private static final UnaryOperator<Object> IDENTITY = t -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identity() {
return (UnaryOperator<T>) IDENTITY;
}
}
// One instance serves all types — safe because erasure makes T -> Object
UnaryOperator<String> stringId = Functions.identity();
UnaryOperator<Integer> intId = Functions.identity();
// Both point to the SAME object at runtime
Q32. How do Collections utility methods use generics?
A: The java.util.Collections class provides many generic utility methods: sort(List<T> list) where T extends Comparable<? super T>, binarySearch, min, max, unmodifiableList(List<? extends T>), synchronizedList, emptyList(), singletonList(), nCopies(), shuffle(), reverse(), fill(), disjoint(), and frequency(). These methods use bounded type parameters and wildcards to be maximally flexible while maintaining type safety.
Q33. What is the difference between Comparable<T> and Comparator<T>?
A: Comparable<T> is implemented by the class itself (natural ordering) — it defines a single compareTo(T o) method and the class must be modified. Comparator<T> is a separate strategy object (external ordering) — it defines compare(T o1, T o2) and multiple comparators can exist for the same type. Use Comparable when the class has an obvious natural order (Integer, String). Use Comparator when you need multiple orderings, cannot modify the class, or want to sort third-party objects.
Q34. Why does Collections.sort use <T extends Comparable<? super T>> instead of <T extends Comparable<T>>?
A: Using ? super T (a lower bounded wildcard on the Comparable bound) makes sort work with types that implement Comparable on a supertype. For example, if Dog extends Animal and Animal implements Comparable<Animal>, then Dog does not directly implement Comparable<Dog> but it IS comparable (via its supertype). <T extends Comparable<T>> would reject Dog; <T extends Comparable<? super T>> accepts it because Dog's compareTo is defined at Animal level.
Q35. What is the instanceof restriction with generics?
A: You cannot use instanceof with parameterized types because the type information is erased at runtime. obj instanceof List<String> is a compile-time error. You can only use instanceof with the raw type (obj instanceof List) or with the unbounded wildcard (obj instanceof List<?>). To check the element type, you must inspect individual elements.
Q36. What is a bounded type parameter vs a bounded wildcard?
A: A bounded type parameter (<T extends Number>) appears in the method/class declaration and gives the type a name (T) that can be referenced. A bounded wildcard (List<? extends Number>) appears as a type argument and represents an unknown type without naming it. Bounded type parameters are used when the type is returned or used in multiple places; bounded wildcards are used in method parameters where you just need flexibility.
Q37. Can you use generics with arrays? What are the rules?
A: Arrays and generics mix poorly due to differing variance rules. You CANNOT create generic arrays (new T[], new List<String>[]) — compile error. You CAN create unbounded wildcard arrays (new List<?>[10]) — though this is rarely useful. Internally, many generic classes use Object[] and cast with @SuppressWarnings("unchecked"). Arrays.asList and ArrayList use this technique.
Q38. What is the difference between <T extends Foo> in a class vs in a method?
A: In a class (class MyClass<T extends Foo>), the type parameter T is in scope for all non-static members of the class. In a method (<T extends Foo> T doSomething()), the type parameter T is only in scope within that method and is resolved fresh at each call site. Method-level type parameters can coexist with class-level parameters, and method-level ones shadow class-level ones if they share the same name (though this is bad practice).
Q39. How does the compiler determine which type to infer?
A: The Java compiler uses a process called type argument inference with several steps: (1) argument type inference from the types of actual arguments passed; (2) return type inference from the target type (assignment context); (3) applicability inference to determine if a method is applicable; (4) invocation type inference to find the final type arguments. Java 8 introduced poly expression inference allowing target-type information to flow into lambda arguments inside generic method calls.
Q40. What is the generic method signature after erasure?
A: After erasure, all type parameters are replaced by their bounds or Object. A method <T extends Comparable<T>> T max(T a, T b) becomes Comparable max(Comparable a, Comparable b). A method <T> T identity(T t) becomes Object identity(Object t). The compiler inserts checkcast instructions at call sites where the result is used as the specific type.
Q41. Can you have a generic enum?
A: No. Enums cannot be generic. The Java specification explicitly prohibits generic enums. The reason is that enum constants are static fields, and static members cannot use the class-level type parameter. All enum types implicitly extend Enum<E extends Enum<E>> (a recursive bound), but you cannot add your own type parameters to an enum declaration.
Q42. What is the recursive type bound pattern (F-bounded polymorphism)?
A: The recursive type bound <T extends Comparable<T>> (F-bounded polymorphism) means T must be comparable to itself. This pattern ensures that only objects of the same type can be compared. It appears in Java's own Enum<E extends Enum<E>> and Comparable<T>. It is used to express the constraint "type T can be compared to instances of T specifically."
// Recursive bound — T must be comparable to itself
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) throw new IllegalArgumentException("Empty list");
T result = list.get(0);
for (T item : list) {
if (item.compareTo(result) > 0) {
result = item;
}
}
return result;
}
Q43. How do you implement a type-safe heterogeneous container?
A: A type-safe heterogeneous container uses Class<T> tokens as keys instead of a type parameter on the container itself. This pattern allows storing and retrieving objects of different types safely. The Class object serves as both key and type token, and the compiler can verify type safety at each put/get call.
public class TypeSafeMap {
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T value) {
map.put(Objects.requireNonNull(type), type.cast(value));
}
public <T> T get(Class<T> type) {
return type.cast(map.get(type));
}
}
TypeSafeMap tsm = new TypeSafeMap();
tsm.put(String.class, "hello");
tsm.put(Integer.class, 42);
String s = tsm.get(String.class); // "hello" — type safe
Integer i = tsm.get(Integer.class); // 42 — type safe
Q44. What unchecked warnings does the compiler emit related to generics?
A: The compiler emits unchecked warnings for: (1) unchecked cast — casting to a parameterized type, e.g., (List<String>) obj; (2) unchecked call — calling a method on a raw type; (3) unchecked conversion — assigning a raw type to a parameterized type; (4) unchecked overriding — overriding a method that uses a raw type with a generic version; (5) varargs with generic types — creating an array of a non-reifiable type. You can suppress with @SuppressWarnings("unchecked") — document why it is safe.
Q45. How do you suppress unchecked warnings safely?
A: Use @SuppressWarnings("unchecked") on the smallest possible scope (variable declaration, not entire class) and always add a comment explaining why the cast is safe. Verify that no heap pollution is introduced. Never use @SuppressWarnings("unchecked") without understanding why the warning appears.
@SuppressWarnings("unchecked") // safe: internal array never exposed
public <T> T[] toArray(T[] a) {
// We know elementData holds T[] even though erased to Object[]
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
Q46. What is the difference between <T extends Number> and <? extends Number>?
A: <T extends Number> is a bounded type parameter — T has a name and can be referenced in the return type, other parameters, or the method body. <? extends Number> is a bounded wildcard — the type is unknown and cannot be referenced. Use T when you need to relate the type to something else (e.g., return type T); use ? when you only need to read from the structure and don't need to name the type.
Q47. What is the generic method vs generic class tradeoff?
A: A generic class carries its type parameter across all instances and all methods share that context. A generic method has an independent type parameter resolved per call. Use generic methods when the type is only relevant to that method and does not need to be remembered by the object. Use generic classes when the type must be remembered across multiple method calls on the same instance (e.g., a Box<T> that stores and returns the same T).
Q48. How do you pass a generic type to a method at runtime (reflection)?
A: Since generics are erased, you cannot directly pass List<String> as a type token. Instead, pass a Class<T> or a ParameterizedType obtained via reflection (e.g., using TypeToken from Guava or capturing a supertype token). A supertype token exploits the fact that superclass generic information is NOT erased, so new TypeToken<List<String>>(){}.getType() returns a ParameterizedType representing List<String> at runtime.
Q49. What is a supertype token (or TypeToken)?
A: A supertype token is an anonymous subclass whose generic supertype information is preserved in the bytecode. Because Java retains generic information for class declarations (not instances), an anonymous subclass of TypeToken<List<String>> allows you to recover the ParameterizedType List<String> at runtime via getClass().getGenericSuperclass(). Guava's TypeToken and Jackson's TypeReference use this technique.
Q50. Can you use primitives as type arguments?
A: No. Java generics do not support primitive types as type arguments. You must use wrapper types: Integer instead of int, Double instead of double, Boolean instead of boolean, etc. Autoboxing and unboxing handle the conversion automatically. Java's value types (Project Valhalla) aim to eventually enable primitive specialization for generics, but as of Java 21+ LTS this is still evolving.
Q51. What is the significance of T extends Comparable<? super T> in sorting?
A: This pattern ensures that types inheriting their natural ordering from a superclass can be sorted. If Dog extends Animal implements Comparable<Animal>, then Dog is comparable via Animal's compareTo. T extends Comparable<T> would require Dog to implement Comparable<Dog>, which it does not. The ? super T lower bound captures both the case where T implements Comparable<T> directly and where it implements Comparable for any supertype.
Q52. What is a generic stack implementation?
A: A generic stack stores elements of type T using an internal Object[] array (since generic arrays cannot be created) and casts appropriately when retrieving elements.
public class GenericStack<T> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;
@SuppressWarnings("unchecked")
public GenericStack() {
elements = new Object[DEFAULT_CAPACITY];
}
public void push(T item) {
ensureCapacity();
elements[size++] = item;
}
@SuppressWarnings("unchecked")
public T pop() {
if (size == 0) throw new EmptyStackException();
T item = (T) elements[--size];
elements[size] = null; // eliminate obsolete reference
return item;
}
public boolean isEmpty() { return size == 0; }
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
Q53. What is the difference between using an array vs a list internally in a generic class?
A: Internal arrays offer better performance (cache locality, no autoboxing) but require Object[] internally with unchecked casts, and cannot use generic array creation. Internal lists (ArrayList) are simpler and cleaner generic code but have slight overhead. ArrayList itself uses Object[] internally for the same reason. For new generic container classes, using a backing List is simpler and type-safer; arrays are used for performance-critical scenarios.
Q54. What is variance in the context of generics?
A: Variance describes how subtype relationships between parameterized types relate to subtype relationships between their type arguments. Covariance: if A is a subtype of B, then Container<A> is a subtype of Container<B> — achieved with ? extends B. Contravariance: if A is a subtype of B, then Container<B> is a subtype of Container<A> — achieved with ? super A. Invariance: no subtype relationship — the default for Java generics with explicit T. Arrays are natively covariant (String[] is-a Object[]) which can cause runtime exceptions.
Q55. Why are arrays covariant but generics invariant?
A: Arrays were covariant from Java 1.0 as a design decision to allow polymorphic array methods. This is a known flaw — it allows ArrayStoreException at runtime. When generics were designed, invariance was chosen to prevent similar runtime type errors, providing compile-time safety instead. Array covariance is considered a mistake in Java's design that could not be changed for backward compatibility.
Q56. How does Collections.unmodifiableList use wildcards?
A: Collections.unmodifiableList(List<? extends T> list) returns List<T>. The ? extends T allows passing a more specific list (e.g., List<String>) and getting back a List<String> (with T inferred as String). This follows PECS — the source list is a producer (you read from it), so ? extends T is appropriate. The returned unmodifiable view can be typed as List<String> while accepting any subtype list as input.
Q57. What is type witness syntax?
A: A type witness is an explicit specification of type arguments at a generic method call site, using syntax Utils.<String>emptyList(). It is needed when the compiler cannot infer the type from context — e.g., assigning to a raw type, or when passing the result directly to another method. In Java 8+, improved target type inference has reduced the need for type witnesses significantly.
Q58. Can a generic method return a wildcard type?
A: Methods can have return types with wildcards (e.g., List<? extends Number>), but this is usually not recommended as callers get very limited access to the returned collection. Best practice is to return a concrete parameterized type (List<Number>) or use a type parameter (<T extends Number> List<T>). The Effective Java guideline says "do not use bounded wildcards as return types."
Q59. What is the Comparable vs raw Comparable issue?
A: Using raw Comparable (without the type parameter) compiles but loses type safety — compareTo accepts Object and the compiler cannot verify correct types. Using Comparable<T> with the appropriate type ensures compareTo is called only with matching types. Legacy code often uses raw Comparable; modern code should always use the parameterized form.
Q60. How does type inference work with method chains?
A: In Java 8+, type inference propagates through method chains using poly expression rules. The compiler evaluates the entire chain considering target type context. For example, in Stream<String> stream = list.stream().filter(s -> s.length() > 3), the compiler infers that stream() returns Stream<String> from the list's element type, and filter's lambda parameter is String. This chained inference reduces the need for explicit type annotations.
Q61. What is the "get-put principle" related to wildcards?
A: The get-put principle (equivalent to PECS) states: use an extending wildcard (? extends T) when you only GET values from a structure, use a super wildcard (? super T) when you only PUT values into a structure, and use an exact type when you both get and put. This principle guides API design to maximize flexibility for callers while maintaining type safety.
Q62. What is the difference between Class<T> and Class<?>?
A: Class<T> is used in generic methods where the actual class type is known and linked to a type parameter — e.g., <T> T create(Class<T> cls) enables returning T and is type-safe. Class<?> means "a Class of unknown type" — used when you just need a Class object but don't need type safety on the result. The method Class.cast(Object) uses the class token to perform a type-checked cast.
Q63. What is the Enum self-referential bound Enum<E extends Enum<E>>?
A: The Enum class is declared as class Enum<E extends Enum<E>>. This recursive bound means E must be an Enum subtype of itself. This ensures that Enum's methods like compareTo(E o) and Enum.valueOf(Class<T> enumType, String name) are type-safe — compareTo only compares enums of the same type. User code never writes Enum<E extends Enum<E>> directly; this is an implementation detail of the JDK.
Q64. Can an interface have generic default methods?
A: Yes. Interface default methods can use the interface's type parameters and can also declare their own additional type parameters. For example, an interface Converter<F, T> could have a default method <R> Converter<F, R> andThen(Converter<T, R> after). Java 8's Comparator.thenComparing uses this pattern extensively.
Q65. What is the Pair class and its generic implementation pitfalls?
A: A generic Pair<A, B> holds two values of potentially different types. Common pitfalls: (1) making it mutable allows one element to be changed independently, breaking assumptions; (2) equals/hashCode must be properly implemented using both A and B; (3) if A or B is itself parameterized, serialization requires special care due to erasure. Java does not provide a built-in Pair class; Apache Commons and JavaFX provide one.
Q66. What is the Collections.checkedList and why is it useful?
A: Collections.checkedList(List<E> list, Class<E> type) returns a dynamically type-checked view of the list. Each insertion is verified at runtime using Class.cast(), throwing ClassCastException immediately if a wrong type is inserted. This is useful when a List<String> might be accessed via a raw List reference by legacy code — the checked wrapper detects pollution at the point of insertion rather than at a potentially distant retrieval point.
Q67. How does Java handle generic type information in reflection?
A: While erasure removes type info from instances, Java retains generic type information in class files for declarations (fields, method signatures, class hierarchy). You can retrieve this via: Field.getGenericType() returns Type (possibly ParameterizedType), Method.getGenericReturnType() and getGenericParameterTypes(), Class.getGenericSuperclass() and getGenericInterfaces(). Cast to ParameterizedType and call getActualTypeArguments() to get the type arguments.
Q68. What is the difference between ParameterizedType, WildcardType, and TypeVariable in reflection?
A: These are sub-interfaces of java.lang.reflect.Type: ParameterizedType represents a generic type with actual arguments — e.g., List<String> has getRawType()=List.class and getActualTypeArguments()=[String.class]. TypeVariable represents a type parameter — T, E, K — with getName() and getBounds(). WildcardType represents a wildcard — ? extends Number — with getUpperBounds() and getLowerBounds(). GenericArrayType represents generic arrays — T[]. They allow full inspection of generic signatures at runtime.
Q69. What is the Optional<T> class and how does it use generics?
A: Optional<T> (Java 8) is a generic container that may or may not hold a non-null value of type T. It provides methods like map(Function<? super T, ? extends U> mapper) which returns Optional<U>, and flatMap(Function<? super T, Optional<U>> mapper). The use of ? super T (PECS — consumer) and ? extends U (producer) in these method signatures ensures maximum flexibility for callers while maintaining type safety across transformation chains.
Q70. What is the Function<T, R> and how does compose/andThen use generics?
A: Function<T, R> represents a function from T to R. The andThen method has signature: <V> Function<T, V> andThen(Function<? super R, ? extends V> after). The ? super R ensures that the after function accepts at least R (PECS — R is produced by this function, consumed by after). The ? extends V ensures the return can be typed as V. This design makes function composition type-safe and maximally flexible.
Q71. What is Stream<T> and how do terminal/intermediate operations use generics?
A: Stream<T> is a generic sequential or parallel pipeline. Intermediate operations like filter(Predicate<? super T>) use PECS (T is produced by the stream, consumed by the predicate). map(Function<? super T, ? extends R>) transforms Stream<T> to Stream<R>. Terminal operations like collect(Collector<? super T, A, R>) use ? super T for the same reason. This careful use of bounded wildcards makes streams work with method references on supertypes.
Q72. What is the difference between a covariant and invariant generic container?
A: An invariant container like List<T> accepts only the exact type T — List<Integer> and List<Number> are unrelated. A covariant container (achieved via ? extends T) accepts T or subtypes — List<? extends Number> accepts List<Integer>, List<Double>, etc. Covariant containers are read-only (you can get T, cannot put T). Understanding this distinction prevents the common mistake of expecting List<Integer> to be usable where List<Number> is expected.
Q73. What is the generic version of Iterator?
A: Iterator<E> is a generic interface with methods hasNext(), next() (returning E), and remove(). The Iterable<T> interface requires an Iterator<T> iterator() method, enabling use in enhanced for-each loops. When you implement Iterable<T> on a custom collection, you provide type-safe iteration where the loop variable is automatically typed as T.
Q74. How do you implement a generic binary search tree?
A: A generic BST requires T to be Comparable. The type bound T extends Comparable<T> (or using a separate Comparator<T>) allows the tree to compare elements without knowing their concrete type. This is a classic use case for both generic classes and bounded type parameters.
public class BST<T extends Comparable<T>> {
private T value;
private BST<T> left, right;
public BST(T value) { this.value = value; }
public void insert(T item) {
if (item.compareTo(value) < 0) {
if (left == null) left = new BST<>(item);
else left.insert(item);
} else {
if (right == null) right = new BST<>(item);
else right.insert(item);
}
}
public boolean contains(T item) {
int cmp = item.compareTo(value);
if (cmp == 0) return true;
if (cmp < 0) return left != null && left.contains(item);
return right != null && right.contains(item);
}
}
Q75. What is the difference between a type-safe cast using Class.cast vs explicit cast?
A: Class.cast(Object obj) performs a runtime check using the class token and throws ClassCastException if wrong, but it does NOT produce an unchecked warning. An explicit cast (T) obj bypasses the class token and in a generic context can produce an unchecked warning (since T is erased, the cast becomes (Object) at runtime). Class.cast is preferred in generic code when a Class<T> is available — it is both type-safe and warning-free.
Q76. How does Map.Entry<K, V> use generics?
A: Map.Entry<K, V> is a nested generic interface inside Map<K, V>. It provides getKey() returning K and getValue() returning V. The generic parameterization is inherited from the enclosing Map's K and V parameters. You can retrieve entries via entrySet() which returns Set<Map.Entry<K, V>>, enabling type-safe iteration over key-value pairs without casting.
Q77. What are the common naming conventions for type parameters?
A: Java conventions: T — Type (general purpose), E — Element (collections), K — Key (maps), V — Value (maps), N — Number (numeric), R — Return type (function results), S, U — additional type params when multiple are needed. When a class has multiple type parameters, A and B may be used. These are just conventions — any identifier is legal as a type parameter, but these single uppercase letters are universally recognized.
Q78. What is an invariant, covariant, and contravariant generic type in Java?
A: Invariant: List<T> — only List<exact T> is accepted; no variance. Covariant: List<? extends T> — List<T> or List<subtype-of-T> accepted; read-only. Contravariant: List<? super T> — List<T> or List<supertype-of-T> accepted; write-friendly. Java's arrays are natively covariant; generic types are invariant by default; wildcards add variance on demand.
Q79. What happens if you mix raw types and generics in the same code?
A: Mixing raw types and generics disables generic type checking for the ENTIRE class in that context, not just the raw-typed variable. For example, if you assign a raw List to a List<String> and call methods on it, the compiler allows the raw calls to "contaminate" the checked calls, potentially causing heap pollution. Always avoid raw types in new code; treat all unchecked warnings seriously.
Q80. What is the generic bounded wildcard capture rule?
A: The compiler performs capture conversion on wildcard types when they are passed to generic methods. A wildcard ? in List<?> is captured as a fresh type variable (call it CAP#1) that the compiler tracks within the method. This allows the compiler to know that the element type is consistent within one method call, even though the caller does not know what it is. The "capture of ?" error appears when capture fails due to incompatible contexts.
Q81. How do generic methods help with the Comparable and Comparator APIs?
A: The Comparator interface in Java 8 provides many generic static and default methods: Comparator.comparing(Function<? super T, ? extends U> keyExtractor) returns Comparator<T>. The use of PECS here ensures that the key extractor's input can be a supertype of T (consumer) and output a subtype of U (producer). thenComparing allows chaining comparators type-safely.
Q82. What is a higher-kinded type and does Java support it?
A: A higher-kinded type is a type constructor that takes other type constructors as parameters — e.g., in Haskell, Functor f says f is a type constructor supporting fmap. Java does NOT natively support higher-kinded types. You cannot write <F<_>> to parameterize over type constructors. This is why monad abstractions are awkward in Java — you cannot write a single Monad<M> that works for both Optional and Stream. Workarounds exist but are complex.
Q83. How does Java's type system differ from C++ templates?
A: Java generics are implemented via erasure — one class file for all parameterizations, no specialization. C++ templates generate separate code for each instantiation (specialization), enabling better performance and allowing sizeof(T), new T(), etc. Java generics provide type safety at compile time only; C++ templates provide both compile-time and runtime type information. Java generics have a single shared implementation at runtime; C++ templates produce multiple implementations. Java cannot do integer template parameters (template<int N>) or template specialization.
Q84. What is the generic type bound on Collectors.toList()?
A: Collectors.toList() has signature: public static <T> Collector<T, ?, List<T>> toList(). The T is inferred from the stream's element type. The ? is the intermediate accumulation type (implementation detail). The return List<T> is the final collected result. In Java 16+, Collectors.toUnmodifiableList() has the same generic structure but returns an unmodifiable list.
Q85. What is Collections.emptyList() and why is it type-safe?
A: Collections.emptyList() has signature <T> List<T> emptyList(). It returns a single shared instance (generic singleton factory pattern) cast to List<T>. This is safe because an empty list has no elements, so there is no actual type mismatch possible — the list will never return a mistyped element. The single instance is reused for all T parameterizations, saving memory while remaining type-safe.
Q86. What is Collections.singletonList() and its generic signature?
A: Collections.singletonList(T o) has signature <T> List<T> singletonList(T o). It infers T from the argument and returns an immutable List containing only that element. Since the list is immutable and created with a known T, it is type-safe. Unlike emptyList(), a new instance is created per call since it must hold the element.
Q87. What is a bounded type parameter on a static utility method?
A: Static utility methods use bounded type parameters to express constraints without being tied to a class-level type. For example: public static <T extends Comparable<T>> void sort(List<T> list) — this sorts any list of Comparable elements without requiring the containing class to be generic. The bound <T extends Comparable<T>> ensures that T has a natural ordering, enabling element comparison within the method body.
Q88. How does the Collector interface use multiple type parameters?
A: Collector<T, A, R> uses three type parameters: T is the input element type (what the stream produces), A is the intermediate mutable accumulation type (often hidden/internal), and R is the final result type (what collect() returns). This three-parameter design cleanly separates the input, working-state, and output concerns. The ? in Collector<T, ?, R> hides the accumulation type from callers who don't need to know about it.
Q89. What is the SuppressWarnings annotation and what values relate to generics?
A: @SuppressWarnings is a standard annotation that tells the compiler to suppress specified warnings. Values related to generics: "unchecked" — suppresses unchecked cast and conversion warnings; "rawtypes" — suppresses raw type usage warnings; "varargs" — suppresses varargs with non-reifiable type warnings (prefer @SafeVarargs instead). Always use the narrowest scope and document why the suppression is safe.
Q90. What is a generic interface with a self-referential type (builder pattern)?
A: The builder pattern often uses a self-referential generic type to support method chaining with correct return types in subclass builders. The pattern Builder<B extends Builder<B>> allows setName(String name) to return B (the concrete builder subtype) rather than the abstract builder, enabling fluent APIs without unchecked casts in subclasses.
public abstract class Builder<B extends Builder<B>> {
protected String name;
@SuppressWarnings("unchecked")
public B name(String name) {
this.name = name;
return (B) this; // safe — B is always the concrete subtype
}
public abstract Object build();
}
public class PersonBuilder extends Builder<PersonBuilder> {
private int age;
public PersonBuilder age(int age) {
this.age = age;
return this;
}
public Person build() { return new Person(name, age); }
}
// Fluent chain with correct types — no cast at call site
Person p = new PersonBuilder().name("Alice").age(30).build();
Q91. What is the difference between T[] and List<T> as generic container choices?
A: Arrays (T[]) cannot be directly created in generic code (use Object[] and cast), are covariant (risky), have fixed size, but offer better cache performance and no boxing. List<T> is clean generic code, dynamically resizable, type-safe, and avoids variance issues, but has slight overhead per element access. For generic API design, prefer List<T> over T[] in return types and parameters, especially when the collection will be iterated or modified.
Q92. Can you have a generic annotation?
A: No. Java annotations cannot be generic. The annotation type declaration does not allow type parameters. Annotation elements can use Class<?> as a type but not parameterized types. Attempts to declare @interface MyAnnotation<T> result in a compile error. This limitation exists because annotation values must be compile-time constants and primitive/String/Class/enum/annotation types.
Q93. What is the generic type in Map.computeIfAbsent?
A: Map<K,V>.computeIfAbsent has signature: V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction). The PECS pattern: ? super K because the function consumes the key (K is produced by the map, consumed by the function), ? extends V because the function produces a value (V is consumed by the map, produced by the function). This allows passing a function that accepts a supertype of K and returns a subtype of V.
Q94. What is the generic wildcard in Predicate.and()?
A: Predicate<T>.and has signature: default Predicate<T> and(Predicate<? super T> other). The ? super T bound means other can test a supertype of T. Since T objects are subtypes of ? super T, passing T values to a Predicate<? super T> is safe. This allows composing predicates where the other predicate is defined for a broader type — a predicate on Animal can be combined with a predicate on Dog.
Q95. What is the "producer extends" half of PECS with a concrete example from JDK?
A: The "Producer Extends" part: when a data source produces elements you consume. In Collections.addAll(Collection<? super T> c, T... elements), the elements varargs act as a producer — you read from them. But actually, Collections.copy(dst, src) shows it clearly: src is List<? extends T> — it produces T elements for you to read. You call src.get(i) to read, not src.add(). The extends bound allows List<Integer> to serve as a source where List<Number> is needed.
Q96. What is the "consumer super" half of PECS with a concrete example from JDK?
A: The "Consumer Super" part: when a destination consumes elements you provide. In Collections.copy(List<? super T> dest, List<? extends T> src), dest is ? super T — it consumes T elements (you call dest.set(i, t)). This allows a List<Object> or List<Number> to serve as destination for Number elements. The super bound ensures that whatever the actual type, it can accept T values because T is a subtype of it.
Q97. What is a generic factory method pattern?
A: A generic factory method uses a type parameter to return an instance of a specified type, often using Class<T> as a token. This is safer than casting because the Class object carries runtime type information. The pattern is widely used in frameworks like Spring (applicationContext.getBean(MyService.class)) and JPA (entityManager.find(User.class, id)).
public class Factory {
private static final Map<Class<?>, Object> instances = new HashMap<>();
@SuppressWarnings("unchecked")
public static <T> T getInstance(Class<T> type) {
return type.cast(instances.computeIfAbsent(type, k -> {
try {
return k.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Cannot instantiate " + k, e);
}
}));
}
}
UserService us = Factory.getInstance(UserService.class); // type-safe
Q98. What is the generic observer pattern?
A: The observer pattern with generics uses EventListener<T> where T is the event type, ensuring that listeners receive only the event types they are registered for. This eliminates the need to cast event objects inside listener implementations.
public interface EventListener<T> {
void onEvent(T event);
}
public class EventBus {
private final Map<Class<?>, List<EventListener<?>>> listeners = new HashMap<>();
public <T> void subscribe(Class<T> eventType, EventListener<T> listener) {
listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
}
@SuppressWarnings("unchecked")
public <T> void publish(T event) {
List<EventListener<?>> list = listeners.get(event.getClass());
if (list != null) {
for (EventListener<?> l : list) {
((EventListener<T>) l).onEvent(event);
}
}
}
}
Q99. How do generics interact with serialization?
A: Generics and serialization have several interactions: (1) Type parameters are erased, so a serialized List<String> is indistinguishable from List<Integer> at the byte level — the type parameter is not serialized. (2) Deserialization always produces raw types in generic terms; you must cast and accept unchecked warnings. (3) Generic classes can implement Serializable but must ensure all type parameter values are also Serializable (or declare them with <T extends Serializable>). (4) writeObject/readObject can use non-generic serialized forms for safety.
Q100. What are the best practices for using generics in API design?
A: Key best practices: (1) Use generics for all new collections and container types — never use raw types. (2) Apply PECS for flexibility: ? extends T for producers, ? super T for consumers. (3) Do not use bounded wildcards as return types — prefer concrete parameterized types. (4) Prefer generic methods to methods that take Object and cast. (5) Use @SafeVarargs for safe varargs methods and document why. (6) Use @SuppressWarnings("unchecked") at the narrowest scope with a comment. (7) Prefer List<T> over T[] in APIs. (8) Use type tokens (Class<T>) when runtime type is needed. (9) Apply recursive bounds (<T extends Comparable<T>>) for self-comparable constraints. (10) Prefer existing generic utility types (Optional, Function, etc.) over inventing new ones.
Q101. What is the difference between T extends Number and ? extends Number in method signatures?
A: When you write <T extends Number> void foo(List<T> list), T is captured and named — you can return T, accept T parameters, or relate multiple T uses. When you write void foo(List<? extends Number> list), the type is anonymous — you can read elements as Number but cannot relate the wildcard type to other parameters. If your method only reads from the list and doesn't relate the element type elsewhere, prefer the wildcard form as it's more readable and restricts the API less.
Q102. Can you use a generic type in a switch statement or switch expression?
A: Traditional switch (Java 14 and earlier) can only switch on int, String, and enum — not on generic types. Java 21 pattern matching switch allows switching on type patterns (case Integer i, case String s), but since generics are erased, you switch on the actual runtime class (which is reifiable). You cannot pattern match on case List<String> since the type argument is erased. Use case List l (raw) and then check elements separately.
Q103. How does generic type inference work with lambda expressions?
A: Java 8's improved type inference allows generic types to be inferred from the target type of a lambda. For example, in Function<String, Integer> f = s -> s.length(), the compiler infers that the lambda is Function<String, Integer> from the assignment target. When passing lambdas to generic methods, the compiler uses the method's parameter type (including bounds) to infer the lambda parameter types. This eliminates most explicit type annotations on lambdas.
Q104. What is the "unchecked cast" warning and when can you safely ignore it?
A: An unchecked cast warning occurs when you cast to a parameterized type (e.g., (List<String>) obj). At runtime, the cast only checks the raw type (List), not the type argument. You can safely suppress it when: (1) you control all insertions into the collection and know only String-compatible objects are present; (2) you are implementing a type-safe heterogeneous container using Class.cast() for individual elements; (3) you are implementing the generic singleton factory pattern with an immutable, element-free instance. Always document the invariant that makes it safe.
Q105. What is the behavior of generics with anonymous inner classes?
A: Anonymous inner classes can be generic in two ways: (1) They can implement or extend a generic type with a concrete type argument — new Comparator<String>() { public int compare(String a, String b){...} }. (2) From Java 9, anonymous classes can use the diamond operator — new Box<>(){...} — where the compiler infers the type. Anonymous classes that implement generic interfaces retain the generic supertype information in their class file, making them useful as supertype tokens (TypeToken pattern).
Post a Comment
Add