Definition
- Type parameter – a placeholder like
orthat is replaced with a concrete type at usage:List,Map - Compile-time checking – the compiler rejects attempts to add the wrong type, catching errors before runtime
- No explicit casts – reading from a parameterised collection returns the declared type directly, not Object
- Type erasure – generics exist only at compile time; at runtime,
ListbecomesListfor backward compatibility - Diamond operator – Java 7 allows
new ArrayList<>()where the compiler infers the type from the declaration
At National Bank of India, without generics, the filing drawer accepts anything: cheques, cash, loan forms, even a lunch box. When Rajesh reaches in expecting a cheque, he might grab a lunch box and crash (ClassCastException). With generics, the drawer is labelled "Cheques Only" and the filing clerk (compiler) rejects anything that is not a cheque at filing time, long before anyone gets hurt.
Understanding the Concepts
What are generics and why do we need them?
Before Java 5, collections stored elements as Object. Every retrieval required an explicit cast, and the compiler could not verify that the cast would succeed. If the wrong type was inserted, a ClassCastException occurred at runtime, potentially in production.
Generics solve this by letting you declare the element type at the collection level. The compiler enforces the type constraint and inserts the necessary casts automatically in the bytecode.
package com.bank.demo;
import java.util.ArrayList;
import java.util.List;
public class GenericsBasicDemo {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
// WITHOUT generics (raw type): runtime ClassCastException
List rawList = new ArrayList();
rawList.add("SAV2001");
rawList.add(42); // no compile error!
String first = (String) rawList.get(0); // OK
System.out.println("Raw get(0): " + first);
try {
String second = (String) rawList.get(1); // ClassCastException at RUNTIME
} catch (ClassCastException e) {
System.out.println("Raw get(1): " + e.getClass().getSimpleName());
}
// WITH generics: compile-time safety
List<String> typedList = new ArrayList<>();
typedList.add("SAV2001");
// typedList.add(42); // COMPILE ERROR: incompatible types
String id = typedList.get(0); // no cast needed
System.out.println("Typed get(0): " + id);
}
}
Raw get(0): SAV2001
Raw get(1): ClassCastException
Typed get(0): SAV2001
How do generics work at the language level?
A generic type declaration uses angle brackets with a type parameter. When you instantiate the type with a concrete argument, the compiler replaces the parameter with the argument for type checking.
package com.bank.demo;
import java.math.BigDecimal;
public class GenericClassDemo {
// Generic class: T is the type parameter
static class AccountHolder<T> {
private final String accountId;
private final T balance;
AccountHolder(String accountId, T balance) {
this.accountId = accountId;
this.balance = balance;
}
T getBalance() { return balance; }
@Override
public String toString() {
return accountId + ": " + balance;
}
}
// Generic method: <T> before return type
static <T> T firstNonNull(T a, T b) {
return a != null ? a : b;
}
public static void main(String[] args) {
// T = BigDecimal
AccountHolder<BigDecimal> rajesh = new AccountHolder<>(
"SAV2001", new BigDecimal("500000.00")
);
BigDecimal balance = rajesh.getBalance(); // no cast
System.out.println("Rajesh: " + balance);
// T = String
AccountHolder<String> label = new AccountHolder<>("CUR3001", "Premium Account");
String desc = label.getBalance(); // no cast
System.out.println("Label: " + desc);
// Generic method: T inferred as String
String result = firstNonNull(null, "DEFAULT");
System.out.println("FirstNonNull: " + result);
}
}
Rajesh: 500000.00
Label: Premium Account
FirstNonNull: DEFAULT
How is code without generics different from code with generics?
| Aspect | Without generics (raw) | With generics |
|---|---|---|
| Declaration | List list | List |
| Adding wrong type | Compiles, fails at runtime | Compile error |
| Reading elements | Returns Object, cast needed | Returns declared type |
| Bug detection | Runtime (production) | Compile time (IDE) |
| Code clarity | Unclear what list contains | Self-documenting |
What are common mistakes?
Mistake: Using raw types in new code
// WRONG: raw type, no type safety
List accounts = new ArrayList();
accounts.add("SAV2001");
accounts.add(42); // no error, but corrupts the list
String id = (String) accounts.get(1); // ClassCastException in production!
// CORRECT: parameterised type
List<String> accounts = new ArrayList<>();
accounts.add("SAV2001");
// accounts.add(42); // compile error, caught immediately
String id = accounts.get(0); // no cast needed
Mistake: Ignoring unchecked warnings
// WRONG: suppressing without understanding
@SuppressWarnings("unchecked")
List<String> names = (List<String>) legacyMethod(); // what if it returns List<Integer>?
// CORRECT: validate or use Collections.checkedList
List<String> names = Collections.checkedList(
new ArrayList<>(), String.class
);
names.addAll((List<String>) legacyMethod()); // ClassCastException at add if wrong type
What about the diamond operator?
Java 7 introduced the diamond operator (<>) to reduce verbosity. The compiler infers the type argument from the left-hand side of the assignment.
package com.bank.demo;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
public class DiamondOperatorDemo {
public static void main(String[] args) {
// Before Java 7: type repeated on both sides
Map<String, BigDecimal> old = new HashMap<String, BigDecimal>();
// Java 7+: diamond operator infers type
Map<String, BigDecimal> balances = new HashMap<>();
balances.put("SAV2001", new BigDecimal("500000.00"));
balances.put("CUR3001", new BigDecimal("1200000.00"));
System.out.println("Balances: " + balances);
}
}
Balances: {SAV2001=500000.00, CUR3001=1200000.00}
What about type erasure?
Generics are a compile-time feature only. The compiler checks types, then erases all type parameters before generating bytecode. At runtime, List and List are the same class: ArrayList. This means you cannot use instanceof List or new T() at runtime.
package com.bank.demo;
import java.util.ArrayList;
import java.util.List;
public class TypeErasureBasicDemo {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println("Same class: " + (strings.getClass() == integers.getClass()));
System.out.println("Class name: " + strings.getClass().getName());
}
}
Same class: true
Class name: java.util.ArrayList
What should a senior engineer know?
- Generics enable the Collections Framework. Without generics, every Collection method would accept and return Object, requiring casts everywhere.
- Generic variance. Java generics are invariant:
Listis not a subtype ofList. Use wildcards (? extends,? super) for flexibility. - Reified generics in other languages. C# retains generic type information at runtime (reified). Java erases it. Kotlin uses
inline reifiedfor certain cases. - var (Java 10).
var list = List.of("SAV2001")infersListat compile time, combining type inference with generics.
What are common interview follow-up questions?
- "What problem do generics solve?" : They eliminate runtime ClassCastException by enforcing type safety at compile time.
- "Are generics available at runtime?" : No. Type erasure removes all generic type information. At runtime, List
is just List. - "What is the diamond operator?" :
<>syntax (Java 7) that lets the compiler infer type arguments:new ArrayList<>(). - "Why can't you create a generic array?" : Because arrays are reified (know their type at runtime) but generics are erased.
Visual Overview
Without Generics (Raw Type)
============================
List rawList = new ArrayList();
┌───────────────────────────────────────┐
│ rawList.add("SAV2001") -> OK │
│ rawList.add(42) -> OK (!) │
│ rawList.add(new Date()) -> OK (!) │
│ String s = (String) rawList.get(1) │
│ -> ClassCastException! │
│ Bug found: RUNTIME (production) │
└───────────────────────────────────────┘
With Generics
==============
List<String> typedList = new ArrayList<>();
┌───────────────────────────────────────┐
│ typedList.add("SAV2001") -> OK │
│ typedList.add(42) -> COMPILE │
│ ERROR! │
│ String s = typedList.get(0) -> OK │
│ (no cast needed) │
│ Bug found: COMPILE TIME (IDE) │
└───────────────────────────────────────┘
Key Takeaway
Generics are one of the most important features in Java for writing safe, maintainable code. By parameterising collections and classes with type parameters, the compiler catches type mismatches at compile time instead of letting them cause ClassCastException in production. This eliminates explicit casts, makes code self-documenting, and prevents an entire category of runtime errors.
The trade-off is type erasure: generic type information is removed at compile time for backward compatibility with pre-Java-5 code. At runtime, List
At National Bank of India, Pankaj mandated that every collection in the codebase be fully parameterised. Lists of account identifiers were declared as List
Remember one sentence: generics turn runtime ClassCastException into compile errors by enforcing type constraints on collections and classes at compile time.
---