Java Tutorial
🔍

Java Generics Basics

Java Generics Basics

Generics let you write a class, interface, or method once and use it safely with any type - the type becomes a parameter, just like a method parameter, but for the type system itself. Without generics, a list that stores any object works but loses all type information: you add a String, retrieve an Object, cast it, and hope nothing breaks at runtime. With generics, List<String> tells the compiler exactly what is inside - it prevents you from adding an Integer, removes the need for a cast when you retrieve, and catches mismatches at compile time rather than at three in the morning when a ClassCastException crashes production.

What Are Generics?

A generic type is a class, interface, or method declaration that has one or more type parameters — placeholders written in angle brackets that the caller fills in with a real type when using the class or calling the method.

WITHOUT GENERICS (pre-Java 5 style):

  class Box {
      private Object value;                 // stores anything

      void set(Object value) { ... }
      Object get() { return value; }        // returns Object - caller must cast
  }

  Box box = new Box();
  box.set("Hello");
  String s = (String) box.get();            // cast required - unsafe
  box.set(42);                              // no error - Integer also goes in
  String broken = (String) box.get();       // ClassCastException at RUNTIME

WITH GENERICS:

  class Box<T> {
      private T value;                      // T is the type parameter

      void set(T value) { ... }
      T get() { return value; }             // returns T - no cast needed
  }

  Box<String> box = new Box<>();
  box.set("Hello");
  String s = box.get();                     // no cast - compiler knows it's String
  box.set(42);                              // COMPILE ERROR - 42 is not a String

The letter T in Box<T> is called a type parameter. It is a placeholder - a variable for a type. When you write Box<String>, T becomes String throughout the class. When you write Box<Integer>, T becomes Integer. The class definition is written once; the type is supplied by the caller.

Basic Overview - The Four Things Every Developer Needs to Know

1. TYPE PARAMETERS - the placeholder syntax

   Fresher view  : <T> means "some type, to be decided by the caller"
                   You write it once in the declaration; the caller
                   fills it in. T, E, K, V, R are conventional letters
                   but any valid identifier works.

   Deeper view   : type parameters are erased at runtime (covered in
                   the Type Erasure article). At compile time, the
                   compiler uses them to verify type safety. At
                   runtime, a Box<String> and a Box<Integer> are both
                   just "Box" - the type argument is gone.

   Conventions:
     T  - general Type (most common single-type parameter)
     E  - Element (used in collections: List<E>, Set<E>)
     K  - Key (used in maps: Map<K, V>)
     V  - Value (used in maps: Map<K, V>)
     R  - Return type (used in functions: Function<T, R>)
     N  - Number (when T is constrained to number types)

2. RAW TYPES - generics without the type argument

   Fresher view  : List list = new ArrayList() is a "raw type" -
                   it compiles but the compiler cannot check what you
                   put in or take out. Every operation on it is
                   "unchecked" and the compiler warns you.

   Deeper view   : raw types exist only for backward compatibility
                   with pre-Java-5 code. Writing a raw type in new
                   code is never correct - it opts out of all the
                   type safety generics provide. Treat every raw-type
                   compiler warning as an error to fix, not suppress.

3. TYPE SAFETY - what generics enforce at compile time

   Fresher view  : with generics, the compiler knows what type is
                   inside a container and stops you from putting in
                   the wrong type or using the result without a cast.

   Deeper view   : the compiler performs "type checking" at every
                   generic call site. A method declared to return T
                   is checked to actually return the T the caller
                   declared. A method that accepts T is checked to
                   receive only T. These checks happen entirely at
                   compile time - the compiled bytecode has no T,
                   only Object (with implicit casts inserted by
                   the compiler where needed)

4. GENERIC vs PARAMETERIZED TYPE

   Fresher view  : Box<T> is the GENERIC type (the declaration with
                   the placeholder). Box<String> is the PARAMETERIZED
                   type (a specific use with String filling the slot).

   Deeper view   : Box<String> and Box<Integer> are different
                   parameterized types but the SAME generic type.
                   At runtime, after type erasure, they are both
                   represented by the single Class object Box.class.
                   Box<String>.class does not exist. This is why
                   new T() and instanceof T fail at runtime - the
                   T information is not there.

Why Generics Were Added to Java

Java 5 introduced generics in 2004, primarily to solve three problems that plagued pre-generics Java code:

Problem 1 — Unsafe casts everywhere. Every collection returned Object. Reading from a List required casting to the expected type, and any mismatch was only discovered at runtime as a ClassCastException. The cast was not verified at compile time; it was pure trust.

Problem 2 — No compiler assistance on container contents. A List that was meant to hold only String objects had no way to enforce that constraint. Any method receiving that List could add any Object to it, corrupting the assumption of the code that originally populated it.

Problem 3 — Verbose, error-prone boilerplate. Writing type-safe containers before Java 5 required either accepting Object everywhere or writing a separate StringList, IntegerList, OrderList class per type — duplicating all the logic for each.

1// File: PreGenericsProblems.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class PreGenericsProblems { 7 8 public static void main(String[] args) { 9 10 System.out.println("=== Pre-generics: the cast problem ==="); 11 12 // Raw List - stores anything as Object 13 List rawList = new ArrayList(); 14 rawList.add("Laptop"); 15 rawList.add("Tablet"); 16 rawList.add(42); // Integer sneaks in - no warning from the compiler here 17 18 for (Object item : rawList) { 19 // Every retrieval needs a cast 20 // The cast to String on an Integer causes ClassCastException at runtime 21 try { 22 String product = (String) item; 23 System.out.println("Product: " + product); 24 } catch (ClassCastException e) { 25 System.out.println("ClassCastException on item: " + item 26 + " (" + item.getClass().getSimpleName() + ")"); 27 } 28 } 29 30 System.out.println(); 31 32 System.out.println("=== With generics: compile-time safety ==="); 33 34 // Parameterized List - stores only String 35 List<String> productList = new ArrayList<>(); 36 productList.add("Laptop"); 37 productList.add("Tablet"); 38 // productList.add(42); <- COMPILE ERROR: incompatible types 39 // The compiler stops this before the program even runs 40 41 for (String product : productList) { 42 // No cast needed - the compiler already knows it is a String 43 System.out.println("Product: " + product); 44 } 45 } 46}
Output:
=== Pre-generics: the cast problem ===
Product: Laptop
Product: Tablet
ClassCastException on item: 42 (Integer)

=== With generics: compile-time safety ===
Product: Laptop
Product: Tablet

How Generic Types Work

Declaring a Generic Class

A generic class declares one or more type parameters in angle brackets immediately after the class name. These parameters can then be used anywhere in the class body where a type would normally appear - as field types, method parameter types, return types, and local variable types.

1// File: GenericBoxDemo.java 2 3public class GenericBoxDemo { 4 5 // T is the type parameter - a placeholder for whatever type the caller specifies 6 static class Box<T> { 7 private T content; 8 9 Box(T content) { 10 this.content = content; 11 } 12 13 T getContent() { 14 return content; // returns exactly T - no Object, no cast 15 } 16 17 void setContent(T content) { 18 this.content = content; 19 } 20 21 boolean isEmpty() { 22 return content == null; 23 } 24 25 @Override 26 public String toString() { 27 return "Box<" + (content == null ? "empty" : content.getClass().getSimpleName()) 28 + ">: " + content; 29 } 30 } 31 32 public static void main(String[] args) { 33 34 // T is bound to String for this instance 35 Box<String> stringBox = new Box<>("Wireless Headphones"); 36 System.out.println(stringBox); 37 String item = stringBox.getContent(); // no cast needed 38 System.out.println("Retrieved: " + item.toUpperCase()); // can call String methods directly 39 40 System.out.println(); 41 42 // T is bound to Integer for this instance 43 Box<Integer> intBox = new Box<>(4999); 44 System.out.println(intBox); 45 int price = intBox.getContent(); // auto-unboxes Integer to int - no cast 46 System.out.println("Price + GST: Rs." + (price * 1.18)); 47 48 System.out.println(); 49 50 // T is bound to Box<String> - generics can be nested 51 Box<Box<String>> boxOfBox = new Box<>(stringBox); 52 System.out.println("Outer: " + boxOfBox); 53 System.out.println("Inner: " + boxOfBox.getContent().getContent()); 54 55 System.out.println(); 56 57 // COMPILE ERRORS - uncommenting either line would fail at compile time 58 // Box<String> broken = new Box<>(42); // 42 is not a String 59 // Integer wrong = stringBox.getContent(); // getContent() returns String, not Integer 60 System.out.println("Type safety verified at compile time - no errors above"); 61 } 62}
Output:
Box<String>: Wireless Headphones
Retrieved: WIRELESS HEADPHONES

Box<Integer>: 4999
Price + GST: Rs.5898.82

Outer: Box<Box>: Box<String>: Wireless Headphones
Inner: Wireless Headphones

Type safety verified at compile time - no errors above

Multiple Type Parameters

A class can declare multiple type parameters, each separated by a comma. Map<K, V> is the canonical example from the JDK - two distinct type parameters, one for keys and one for values.

1// File: MultipleTypeParamsDemo.java 2 3public class MultipleTypeParamsDemo { 4 5 // Two type parameters - K for key, V for value 6 static class Pair<K, V> { 7 private final K key; 8 private final V value; 9 10 Pair(K key, V value) { 11 this.key = key; 12 this.value = value; 13 } 14 15 K getKey() { return key; } 16 V getValue() { return value; } 17 18 @Override 19 public String toString() { 20 return "Pair(" + key + ", " + value + ")"; 21 } 22 } 23 24 public static void main(String[] args) { 25 // K=String, V=Integer 26 Pair<String, Integer> productPrice = new Pair<>("Laptop Stand", 1299); 27 System.out.println(productPrice); 28 System.out.println("Key type : " + productPrice.getKey().getClass().getSimpleName()); 29 System.out.println("Value type: " + productPrice.getValue().getClass().getSimpleName()); 30 31 System.out.println(); 32 33 // K=Integer, V=String - order matters 34 Pair<Integer, String> idToStatus = new Pair<>(1001, "SHIPPED"); 35 System.out.println(idToStatus); 36 37 System.out.println(); 38 39 // K=String, V=Pair<String,Integer> - nested parameterization 40 Pair<String, Pair<String, Integer>> productDetail = 41 new Pair<>("PROD-501", new Pair<>("Wireless Mouse", 799)); 42 System.out.println(productDetail); 43 System.out.println("Inner value: " + productDetail.getValue().getValue()); 44 } 45}
Output:
Pair(Laptop Stand, 1299)
Key type  : String
Value type: Integer

Pair(1001, SHIPPED)

Pair(PROD-501, Pair(Wireless Mouse, 799))
Inner value: 799

Raw Types and Why They Are Dangerous

A raw type is a generic class used without its type argument - Box instead of Box<String>. The compiler accepts it (for backward compatibility with pre-Java-5 code) but issues unchecked warnings, and the entire type-safety guarantee is gone.

1// File: RawTypeDemo.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class RawTypeDemo { 7 8 static class Box<T> { 9 private T content; 10 Box(T content) { this.content = content; } 11 T getContent() { return content; } 12 } 13 14 public static void main(String[] args) { 15 16 System.out.println("=== Raw type - all safety lost ==="); 17 18 // Raw type - no type argument. Compiler warns but compiles. 19 Box rawBox = new Box("original string"); // unchecked warning 20 rawBox = new Box(12345); // reassign to Integer - no warning 21 22 // The compiler cannot know what getContent() returns - it becomes Object 23 Object rawContent = rawBox.getContent(); 24 System.out.println("Raw content: " + rawContent); 25 26 // This cast is UNCHECKED - if the type is wrong, ClassCastException at runtime 27 try { 28 String wrongCast = (String) rawBox.getContent(); // ClassCastException 29 } catch (ClassCastException e) { 30 System.out.println("ClassCastException: raw type had Integer, cast expected String"); 31 } 32 33 System.out.println(); 34 35 System.out.println("=== Parameterized type - safety restored ==="); 36 37 // Parameterized - compiler knows T=String 38 Box<String> typedBox = new Box<>("original string"); 39 // typedBox = new Box<>(12345); // COMPILE ERROR - cannot reassign to Box<Integer> 40 String typedContent = typedBox.getContent(); // no cast, no exception possible 41 System.out.println("Typed content: " + typedContent.toUpperCase()); 42 43 System.out.println(); 44 45 System.out.println("=== Heap pollution - mixing raw and generic ==="); 46 List<String> names = new ArrayList<>(); 47 names.add("Ananya"); 48 names.add("Rahul"); 49 50 // Assigning to a raw List reference "loses" the type information 51 // but the underlying List<String> object is unchanged 52 List rawNames = names; // unchecked assignment warning 53 rawNames.add(42); // Integer added to a List<String> - no immediate error 54 55 // The ClassCastException appears when the Integer is READ through 56 // the typed reference - the bug appears far from where it was caused 57 try { 58 for (String name : names) { // ClassCastException on Integer element 59 System.out.println(name); 60 } 61 } catch (ClassCastException e) { 62 System.out.println("Heap pollution: " + e.getMessage()); 63 } 64 } 65}
Output:
=== Raw type - all safety lost ===
Raw content: 12345
ClassCastException: raw type had Integer, cast expected String

=== Parameterized type - safety restored ===
Typed content: ORIGINAL STRING

=== Heap pollution - mixing raw and generic ===
Ananya
Rahul
Heap pollution: class java.lang.Integer cannot be cast to class java.lang.String

The Compiler's Role in Generic Type Safety

WHAT THE COMPILER DOES WITH GENERICS:

  Box<String> box = new Box<>("hello");

  Step 1 - TYPE ARGUMENT CAPTURE:
    The compiler records T = String for this Box instance.

  Step 2 - CALL SITE VERIFICATION:
    Every call to box.set(X) is checked: is X a String?
    Every call to box.get() is typed as String, not Object.

  Step 3 - IMPLICIT CAST INSERTION:
    box.get() returns T, which compiles to Object in bytecode.
    The compiler inserts a CHECKCAST instruction automatically,
    so "String s = box.get()" becomes "String s = (String) box.get()"
    in the compiled bytecode - but only after verifying no type error
    exists at the source level.

  Step 4 - ERASURE:
    T is replaced with Object (or its bound, if bounded) in bytecode.
    Box<String> and Box<Integer> compile to identical bytecode.
    At runtime: box.getClass() == Box.class, always.

THE TYPE SYSTEM'S GUARANTEE:
  If a Java program compiles without unchecked warnings, and all
  libraries it uses were also compiled without unchecked warnings,
  then no ClassCastException from generic code can occur at runtime
  (unless a cast is explicitly written in application code).
  The casts are there in the bytecode - they are just correct,
  because the compiler verified they must be.

Real-World Example - Flipkart Generic Repository Pattern

A product catalog service needs to perform the same operations - find by ID, save, delete, and list all - on several different entity types: Product, Seller, Review. Without generics, a separate repository class with identical code would need to be written for each type. With generics, one Repository<T, ID> captures the entire pattern, and each concrete repository specifies its types.

1// File: Repository.java 2 3import java.util.List; 4import java.util.Optional; 5 6// T - the entity type this repository manages 7// ID - the type of the entity's identifier 8public interface Repository<T, ID> { 9 void save(T entity); 10 Optional<T> findById(ID id); 11 List<T> findAll(); 12 void delete(ID id); 13 int count(); 14}
1// File: Product.java 2 3public class Product { 4 private final String productId; 5 private final String name; 6 private final double price; 7 8 public Product(String productId, String name, double price) { 9 this.productId = productId; 10 this.name = name; 11 this.price = price; 12 } 13 14 public String getProductId() { return productId; } 15 public String getName() { return name; } 16 public double getPrice() { return price; } 17 18 @Override 19 public String toString() { 20 return "Product[" + productId + ", " + name + ", Rs." + price + "]"; 21 } 22}
1// File: InMemoryProductRepository.java 2 3import java.util.*; 4 5// T=Product, ID=String - this class works only with Product entities 6// identified by String IDs. The Repository<T,ID> contract is satisfied 7// by filling in both type parameters concretely. 8public class InMemoryProductRepository implements Repository<Product, String> { 9 10 private final Map<String, Product> store = new LinkedHashMap<>(); 11 12 @Override 13 public void save(Product product) { 14 store.put(product.getProductId(), product); 15 } 16 17 @Override 18 public Optional<Product> findById(String productId) { 19 return Optional.ofNullable(store.get(productId)); 20 } 21 22 @Override 23 public List<Product> findAll() { 24 return List.copyOf(store.values()); 25 } 26 27 @Override 28 public void delete(String productId) { 29 store.remove(productId); 30 } 31 32 @Override 33 public int count() { 34 return store.size(); 35 } 36}
1// File: GenericRepositoryDemo.java 2 3public class GenericRepositoryDemo { 4 5 public static void main(String[] args) { 6 InMemoryProductRepository repo = new InMemoryProductRepository(); 7 8 System.out.println("=== Saving products ==="); 9 repo.save(new Product("PROD-001", "Wireless Headphones", 2499.0)); 10 repo.save(new Product("PROD-002", "Laptop Stand", 1299.0)); 11 repo.save(new Product("PROD-003", "USB-C Hub", 1799.0)); 12 System.out.println("Saved " + repo.count() + " products"); 13 14 System.out.println(); 15 16 System.out.println("=== Finding by ID ==="); 17 repo.findById("PROD-002").ifPresent(p -> 18 System.out.println("Found: " + p)); 19 repo.findById("PROD-999").ifPresentOrElse( 20 p -> System.out.println("Found: " + p), 21 () -> System.out.println("PROD-999: not found")); 22 23 System.out.println(); 24 25 System.out.println("=== Listing all products ==="); 26 repo.findAll().forEach(p -> System.out.println(" " + p)); 27 28 System.out.println(); 29 30 System.out.println("=== Deleting a product ==="); 31 repo.delete("PROD-001"); 32 System.out.println("Remaining count: " + repo.count()); 33 repo.findAll().forEach(p -> System.out.println(" " + p)); 34 } 35}
Output:
=== Saving products ===
Saved 3 products

=== Finding by ID ===
Found: Product[PROD-002, Laptop Stand, Rs.1299.0]
PROD-999: not found

=== Listing all products ===
  Product[PROD-001, Wireless Headphones, Rs.2499.0]
  Product[PROD-002, Laptop Stand, Rs.1299.0]
  Product[PROD-003, USB-C Hub, Rs.1799.0]

=== Deleting a product ===
Remaining count: 2
  Product[PROD-002, Laptop Stand, Rs.1299.0]
  Product[PROD-003, USB-C Hub, Rs.1799.0]

Repository<T, ID> is written once. InMemoryProductRepository uses it with Product and String. A SellerRepository would use it with Seller and Long. A ReviewRepository with Review and UUID. Zero duplication of the contract definition. This is the pattern Spring Data JPA's JpaRepository<T, ID> is built on - the same interface the whole industry uses for repository-layer design.

Generic Naming Conventions

LetterMeaningCommon Use
TTypeMost general single type parameter - Box<T>, Optional<T>
EElementTypes that represent elements in collections - List<E>, Set<E>
KKeyKey in a key-value pair - Map<K, V>
VValueValue in a key-value pair - Map<K, V>
RReturn typeReturn type in functional interfaces - Function<T, R>
NNumberWhen the type is constrained to numeric types
AAccumulatorUsed in collectors and fold operations
S, USecond, thirdWhen multiple types are needed beyond T - BiFunction<T, U, R>

These are conventions, not rules enforced by the compiler. Box<Foo> compiles. But deviating from the convention without reason makes code harder to read - readers expect E in collection-like classes and K, V in map-like ones.

Best Practices

Always use the specific parameterized type rather than the raw type. List<String> instead of List. Map<String, Integer> instead of Map. The raw type silences every type-safety guarantee generics provide, makes code harder to read, and produces unchecked warnings that accumulate in the build output until everyone stops noticing them. Every unchecked warning is a potential ClassCastException deferred to runtime.

Use standard naming conventions for type parameters. T, E, K, V, R are recognized instantly by any Java developer. A parameter named MyType or DataObject requires the reader to stop and decide whether it is a concrete type or a type parameter, which is a completely avoidable cognitive load.

Keep type parameter lists short. One or two type parameters is normal. Three is occasionally necessary (some function types in streams). More than three is almost always a sign that the design can be simplified - either the class is doing too much, or some of the type relationships can be expressed through bounded parameters rather than separate free variables.

Understand that generics are a compile-time feature only. There is no List<String> at runtime - only List. This is not a limitation to work around; it is the design. The implications (no new T(), no instanceof T, no T[]) are covered fully in the Type Erasure and What Generics Cannot Do articles. The practical takeaway here: do not expect runtime type information from generics.

Common Mistakes

Mistake 1 - Using Raw Types in New Code

1import java.util.ArrayList; 2import java.util.List; 3 4// WRONG - raw List accepts everything, returns Object, 5// and produces unchecked warnings throughout 6List products = new ArrayList(); 7products.add("Laptop"); 8products.add(1299); // no error - Integer added silently 9 10// Every retrieval needs an unsafe cast 11String name = (String) products.get(0); // works here by luck 12String price = (String) products.get(1); // ClassCastException at runtime 13 14// CORRECT - parameterized type enforces the constraint at compile time 15List<String> productNames = new ArrayList<>(); 16productNames.add("Laptop"); 17// productNames.add(1299); // COMPILE ERROR - caught immediately 18String productName = productNames.get(0); // no cast needed

Mistake 2 - Treating Generic Type Arguments as Inherited Types

1import java.util.ArrayList; 2import java.util.List; 3 4// WRONG ASSUMPTION - because Integer extends Number, a developer 5// might expect List<Integer> to be usable where List<Number> is required. 6// It is NOT. Generic types are INVARIANT - List<Integer> and List<Number> 7// are completely unrelated types even though Integer extends Number. 8static void printNumbers(List<Number> numbers) { 9 numbers.forEach(System.out::println); 10} 11 12List<Integer> integers = List.of(1, 2, 3); 13// printNumbers(integers); // COMPILE ERROR 14// "incompatible types: List<Integer> cannot be converted to List<Number>" 15 16// CORRECT - use a bounded wildcard if any Number subtype should work. 17// Wildcards are covered in their own articles; here is the fix: 18static void printAnyNumbers(List<? extends Number> numbers) { 19 numbers.forEach(System.out::println); 20} 21 22printAnyNumbers(integers); // now works - Integer IS-A Number satisfies ? extends Number

Mistake 3 - Confusing Parameterized Instances With Their Types

1import java.util.ArrayList; 2import java.util.List; 3 4// WRONG - a common beginner assumption is that Box<String>.class 5// exists as a distinct Class object from Box<Integer>.class 6// At runtime, after type erasure, there is ONLY Box.class 7class Box<T> { T value; } 8 9// This does not compile: 10// Class<Box<String>> c = Box<String>.class; // SYNTAX ERROR - not valid 11 12// Box<String> and Box<Integer> have the SAME class at runtime: 13Box<String> stringBox = new Box<>(); 14Box<Integer> intBox = new Box<>(); 15System.out.println(stringBox.getClass() == intBox.getClass()); // prints: true 16System.out.println(stringBox.getClass().getName()); // prints: Box (no type argument) 17 18// CORRECT understanding: the type argument exists only at compile time. 19// At runtime, instanceof and .class see only the raw class. 20// Use instanceof with the raw class: 21System.out.println(stringBox instanceof Box); // true - raw type check is valid 22// System.out.println(stringBox instanceof Box<String>); // COMPILE ERROR in most contexts

Mistake 4 - Ignoring Unchecked Warnings With @SuppressWarnings("unchecked")

1import java.util.ArrayList; 2import java.util.List; 3 4// WRONG - suppressing unchecked warnings without understanding why 5// they appear. Each one is a potential ClassCastException at runtime. 6// Blanket suppression hides real bugs. 7@SuppressWarnings("unchecked") 8static List<String> unsafeConvert(List<?> input) { 9 return (List<String>) input; // unsafe cast - ClassCastException if input is List<Integer> 10} 11 12// CORRECT - when an unchecked operation is unavoidable (rare, usually 13// in framework-level generic utilities), document WHY it is safe and 14// use the narrowest possible scope for the suppression 15@SuppressWarnings("unchecked") 16static <T> T firstElement(List<?> list) { 17 // Safe: this method's caller controls T and the list type at the 18 // call site. If misused, the ClassCastException will appear at 19 // the call site, not buried here. 20 return (T) list.get(0); 21}

Interview Questions

Q1. What are generics in Java, and what problem do they solve?

Generics allow classes, interfaces, and methods to declare type parameters - placeholders for types that the caller specifies at the usage site. They solve three problems that existed before Java 5: unsafe casts that caused ClassCastException at runtime when a collection returned Object, no compile-time checking of what types went into a container, and the need to write separate classes for each type a container needed to support. With generics, List<String> tells the compiler exactly what is inside - it prevents adding the wrong type, eliminates the need for casts when reading, and turns type mismatches from runtime errors into compile errors.

Q2. What is the difference between a generic type and a parameterized type?

A generic type is the declaration - class Box<T> or interface Repository<T, ID> - with one or more type parameters as placeholders. A parameterized type is a specific use of a generic type with real types supplied for the parameters - Box<String>, Repository<Product, Long>. The generic type is written once; parameterized types are the concrete uses. At runtime, after type erasure, all parameterized types of the same generic type share a single Class object - Box<String> and Box<Integer> are both just Box.class at runtime.

Q3. What is a raw type in Java, and why is it dangerous?

A raw type is a generic class or interface used without its type argument - List instead of List<String>, Box instead of Box<Integer>. The compiler accepts raw types for backward compatibility with pre-Java-5 code but issues unchecked warnings. A raw type is dangerous because it loses all compile-time type safety: the compiler cannot check what is put into or retrieved from it, every retrieval returns Object requiring an explicit cast, and wrong-type elements can be added without compile-time error, causing ClassCastException at runtime - often far from the point where the incorrect element was inserted. Writing raw types in new code is never correct.

Q4. Why is List not a subtype of List, even though Integer extends Number?

Generic types in Java are invariant - List<Integer> and List<Number> are completely unrelated types even though Integer is a subtype of Number. The reason is type safety: if List<Integer> were a List<Number>, you could assign a List<Integer> to a List<Number> reference and then add a Double (which is also a Number) to it - corrupting the original List<Integer>. Java's type system prevents this by making the relationship between generic parameterizations invariant. When covariant behavior is intentional, bounded wildcards (List<? extends Number>) are the correct mechanism - they allow reading but prevent writing.

Q5. What naming conventions are used for type parameters, and why do they matter?

The conventional single-letter names are T for a general type, E for element (used in collections), K and V for key and value (used in maps), and R for return type (used in functional interfaces). These conventions matter because they communicate the intended role of the type parameter immediately to any Java developer reading the code. Map<K, V> is instantly understood; Map<KeyType, ValueType> requires a reader to verify whether KeyType is a concrete class or a type parameter. Conventions reduce cognitive load - readers should not need to look up declarations to understand whether a name is a type or a type parameter.

Q6. How does the compiler enforce type safety with generics, and what does it mean that generics are a compile-time feature?

The compiler tracks the type argument supplied at each generic usage site and uses it to verify every operation on that parameterized type - every argument passed to a generic method, every value returned from one. When Box<String> is used, the compiler knows get() returns String and set() accepts only String. It inserts implicit CHECKCAST bytecode instructions where needed and rejects type mismatches as compile errors. At runtime, after type erasure, the type argument is gone - Box<String> and Box<Integer> compile to identical bytecode, both using Object where T appeared. The guarantee is entirely compile-time: if the code compiled without unchecked warnings, the inserted casts will not fail, because the compiler already verified they are correct.

FAQs

Can I use primitive types like int or double as type arguments?

No - type arguments must be reference types. Box<int> is a compile error; Box<Integer> is correct. Java's autoboxing and unboxing handle the conversion automatically in most cases: Box<Integer> box = new Box<>(42) compiles because 42 is autoboxed to Integer. The restriction exists because generics use Object as the erased type internally, and primitives are not Object. For performance-critical code with large numbers of primitive values, collections like IntStream or third-party libraries with primitive specializations avoid the boxing overhead.

Can a type parameter be used as the type of a static field?

No. A static field belongs to the class itself, not to any instance, and all instances share the same static field. Box<String> and Box<Integer> are different parameterized types but the same class at runtime - a static field on the class cannot be both String and Integer for different parameterizations simultaneously. The compiler therefore rejects static T staticField inside class Box<T>. Static methods can, however, declare their own independent type parameters: static <T> Box<T> empty() is valid because T here belongs to the method, not the class.

Does ArrayList have a different Class object than ArrayList at runtime?

No. Both have the same Class object - java.util.ArrayList.class. This is type erasure: the type argument String or Integer exists only in the source code and in the compiler's type-checking phase. After compilation, both are represented by the same raw bytecode class. This is why list.getClass() returns ArrayList.class for both, and why new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() evaluates to true.

What is heap pollution in the context of generics?

Heap pollution occurs when a variable of a parameterized type refers to an object that is not actually of that type - for example, when a List<String> reference ends up pointing to a list that actually contains non-String elements, because somewhere raw types or unsafe casts were used to bypass the type system. The pollution is called "heap" because the problem exists in the runtime heap state even though the type parameter information is supposed to express what is there. It typically surfaces as a ClassCastException in code that only reads from the collection and never performs any explicit cast - the cast was inserted by the compiler as part of generic type erasure, and it fails because the heap state was corrupted earlier through a raw-type or unchecked operation.

Can a generic class extend another generic class?

Yes, and there are two common patterns. A generic subclass can pass its own type parameter to the parent: class TypedBox<T> extends Box<T> makes TypedBox<String> extend Box<String>. Alternatively, a concrete subclass can fix the parent's type parameter: class StringBox extends Box<String> creates a non-generic class that is always a Box<String>. Both are valid and appear throughout the JDK — ArrayList<E> extends AbstractList<E> (first pattern), while Properties extends Hashtable<Object,Object> (second pattern).

What is the wildcard <?> and how does it differ from a type parameter ?

A type parameter <T> is declared by the class or method and is bound to a specific type at the usage site — Box<T> lets the caller choose T. A wildcard <?> is used at a call site to say "some type, I don't know or care which" — Box<?> accepts a Box of any type but does not name or bind that type, so you cannot add to or read typed values from it. A type parameter is about declaration; a wildcard is about consumption. Wildcards are covered in depth in the Wildcards article in this series.

Summary

Generics are Java's answer to writing type-safe, reusable code. A type parameter - the <T> in Box<T> or Repository<T, ID> - is a placeholder that the caller fills in when using the class or calling the method. The compiler tracks that type argument at every usage site, prevents type mismatches at compile time, and inserts verified casts where the bytecode needs them. What makes it work is entirely a compile-time mechanism: at runtime, after type erasure, Box<String> and Box<Integer> are both just Box.

The single most important habit this topic builds: reach for the parameterized type every time, never the raw type. List<String> over List. Map<String, Integer> over Map. Each raw type is a silent withdrawal from the type-safety contract that makes the rest of the code reliable - and the ClassCastException it eventually causes will appear somewhere completely unrelated to the raw type that caused it.

Everything else in the Generics series - bounded type parameters, wildcards, PECS, type erasure, and the restrictions that follow from it - builds directly on the foundation this article covers.

What to Read Next