Back to Generics & Type Safety
Q1Question 1 of 7

What are generics and why do we need them?

Key Concepts
  • Generics provide compile-time type safety, turning runtime ClassCastException into compile errors that are caught before the code runs.
  • Without generics, collections store Object and require explicit casting on retrieval, which can fail silently at runtime.
  • With generics, the compiler verifies that only the declared type is added and removes the need for explicit casts.
  • Generics were introduced in Java 5 and use type erasure for backward compatibility: type parameters are removed at compile time.
  • The diamond operator (<>) in Java 7 allows type inference on the right side: new ArrayList<>() instead of new ArrayList<String>().
  • Generics apply to classes, interfaces, methods, and constructors, not just collections.
  • Raw types (List instead of List<String>) should never be used in new code because they bypass type safety.

Definition

Generics provide compile-time type safety by parameterising classes and methods with type parameters, eliminating explicit casts and converting runtime ClassCastException into compile errors.
  • Type parameter – a placeholder like or that 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, List becomes List for 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.

JAVA
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);
    }
}
TEXT
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.

JAVA
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);
    }
}
TEXT
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 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

JAVA
// 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!
JAVA
// 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

JAVA
// WRONG: suppressing without understanding
@SuppressWarnings("unchecked")
List<String> names = (List<String>) legacyMethod(); // what if it returns List<Integer>?
JAVA
// 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.

JAVA
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);
    }
}
TEXT
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.

JAVA
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());
    }
}
TEXT
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: List is not a subtype of List. 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 reified for certain cases.
  • var (Java 10). var list = List.of("SAV2001") infers List at 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

    DIAGRAM
      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 and List are the same class. This means certain operations (new T(), instanceof List, T.class) are not possible, which is covered in the type erasure question.

    At National Bank of India, Pankaj mandated that every collection in the codebase be fully parameterised. Lists of account identifiers were declared as List, maps of balances as Map. This meant that mixing account IDs with balance amounts was caught by the compiler, not by a production incident report.

    Remember one sentence: generics turn runtime ClassCastException into compile errors by enforcing type constraints on collections and classes at compile time.

    ---

    Share
    Use ← → arrow keys to navigate