Java Generics

Learn how Generics enable you to write flexible, reusable, and type-safe code that works with any data type while catching errors at compile-time!

What are Generics?

Generics allow you to write classes, interfaces, and methods that work with different data types while providing compile-time type safety. Think of it like a container that can hold any type of object, but once you specify the type, it ensures you only put that type inside!

Before Generics (Java 5), collections could store any object type, leading to runtime errors. With Generics, you specify the type at compile-time, catching errors early and eliminating the need for type casting.

  • Type Safety: Catch type-related errors at compile-time, not runtime
  • No Type Casting: Eliminates the need for explicit type casting
  • Code Reusability: Write once, use with multiple types
  • Cleaner Code: Makes code more readable and maintainable
  • Collections Framework: Heavily uses Generics (ArrayList<T>, HashMap<K,V>)

๐Ÿ’ก Simple Analogy: Generics are like labeled boxes. A box labeled "Books" should only contain books, not toys. The label (type parameter) ensures you don't accidentally put the wrong item inside!

Without Generics vs With Generics

Let's see the difference between code without Generics and with Generics:

Example: Before and After Generics

import java.util.ArrayList;

public class GenericsComparison {
    public static void main(String[] args) {
        
        // WITHOUT GENERICS (Old way - Before Java 5)
        System.out.println("=== Without Generics ===");
        ArrayList list1 = new ArrayList();  // Can store any type
        list1.add("Hello");
        list1.add(10);          // Mixing types - No error!
        list1.add(3.14);
        
        String str1 = (String) list1.get(0);  // Need type casting
        // String str2 = (String) list1.get(1);  // Runtime error!
        System.out.println("First element: " + str1);
        
        // WITH GENERICS (Modern way - Java 5+)
        System.out.println("\n=== With Generics ===");
        ArrayList list2 = new ArrayList<>();  // Only Strings
        list2.add("Hello");
        list2.add("World");
        // list2.add(10);  // Compile-time error! Type safety
        
        String str2 = list2.get(0);  // No casting needed!
        System.out.println("First element: " + str2);
        
        // Type-safe iteration
        System.out.println("\nAll elements:");
        for (String s : list2) {
            System.out.println(s);
        }
    }
}
Output:
=== Without Generics ===
First element: Hello

=== With Generics ===
First element: Hello

All elements:
Hello
World

Explanation:

  • Without Generics: Can add any type, requires casting, errors happen at runtime
  • With Generics: Type-safe, no casting needed, errors caught at compile-time
  • <String> - Type parameter specifies the collection stores only Strings
  • Generics prevent mixing different types accidentally

Generic Class

You can create your own generic classes that work with any type. Here's a simple Box class:

Example: Creating a Generic Class

// Generic class with type parameter T
class Box {
    private T content;
    
    public void set(T content) {
        this.content = content;
    }
    
    public T get() {
        return content;
    }
    
    public void display() {
        System.out.println("Box contains: " + content);
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        
        // Box for Integer
        Box intBox = new Box<>();
        intBox.set(123);
        System.out.println("Integer Box: " + intBox.get());
        intBox.display();
        
        // Box for String
        Box strBox = new Box<>();
        strBox.set("Hello Generics");
        System.out.println("\nString Box: " + strBox.get());
        strBox.display();
        
        // Box for Double
        Box doubleBox = new Box<>();
        doubleBox.set(99.99);
        System.out.println("\nDouble Box: " + doubleBox.get());
        doubleBox.display();
        
        // Type safety - This won't compile:
        // intBox.set("String");  // Error! Expects Integer
    }
}
Output:
Integer Box: 123
Box contains: 123

String Box: Hello Generics
Box contains: Hello Generics

Double Box: 99.99
Box contains: 99.99

Explanation:

  • <T> - Type parameter (T is convention for "Type", can use any name)
  • Box<Integer> - Creates a Box that only accepts Integer type
  • Same class works with different types without code duplication
  • Compiler ensures type safety - can't put wrong type in wrong box

Common Type Parameter Naming Conventions

1. T - Type

The most common type parameter, represents any general type.

Usage: class Box<T>, List<T>

2. E - Element

Used for element types in collections.

Usage: ArrayList<E>, Set<E>

3. K, V - Key and Value

Used for key-value pairs in maps.

Usage: HashMap<K, V>, Map<K, V>

4. N - Number

Represents numeric types.

Usage: class Calculator<N extends Number>

Generic Methods

You can create generic methods that work with different types, even in non-generic classes:

Example: Generic Methods

public class GenericMethodExample {
    
    // Generic method to print array of any type
    public static  void printArray(T[] array) {
        System.out.print("[ ");
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println("]");
    }
    
    // Generic method to find maximum
    public static > T findMax(T a, T b, T c) {
        T max = a;
        if (b.compareTo(max) > 0) max = b;
        if (c.compareTo(max) > 0) max = c;
        return max;
    }
    
    public static void main(String[] args) {
        
        // Print Integer array
        Integer[] intArray = {1, 2, 3, 4, 5};
        System.out.print("Integer Array: ");
        printArray(intArray);
        
        // Print String array
        String[] strArray = {"Apple", "Banana", "Cherry"};
        System.out.print("String Array: ");
        printArray(strArray);
        
        // Print Double array
        Double[] doubleArray = {1.1, 2.2, 3.3};
        System.out.print("Double Array: ");
        printArray(doubleArray);
        
        // Find maximum values
        System.out.println("\nFinding Maximum:");
        System.out.println("Max of 3, 7, 2: " + findMax(3, 7, 2));
        System.out.println("Max of 5.5, 2.3, 8.9: " + findMax(5.5, 2.3, 8.9));
        System.out.println("Max of 'x', 'a', 'm': " + findMax('x', 'a', 'm'));
    }
}
Output:
Integer Array: [ 1 2 3 4 5 ]
String Array: [ Apple Banana Cherry ]
Double Array: [ 1.1 2.2 3.3 ]

Finding Maximum:
Max of 3, 7, 2: 7
Max of 5.5, 2.3, 8.9: 8.9
Max of 'x', 'a', 'm': x

Explanation:

  • <T> before return type declares a generic method
  • printArray() works with arrays of any type
  • <T extends Comparable<T>> - Bounded type parameter (only comparable types)
  • Generic methods provide flexibility without creating separate methods for each type

Multiple Type Parameters

You can use multiple type parameters in a single class or method:

Example: Generic Pair Class

// Generic class with two type parameters
class Pair {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() {
        return key;
    }
    
    public V getValue() {
        return value;
    }
    
    public void display() {
        System.out.println(key + " -> " + value);
    }
}

public class MultipleTypeParams {
    public static void main(String[] args) {
        
        // Country -> Capital
        Pair country = new Pair<>("India", "New Delhi");
        System.out.print("Country-Capital: ");
        country.display();
        
        // Student -> Roll Number
        Pair student = new Pair<>("Alice", 101);
        System.out.print("Student-Roll: ");
        student.display();
        
        // Product -> Price
        Pair product = new Pair<>("Laptop", 45999.99);
        System.out.print("Product-Price: ");
        product.display();
        
        // Accessing values
        System.out.println("\nStudent name: " + student.getKey());
        System.out.println("Roll number: " + student.getValue());
    }
}
Output:
Country-Capital: India -> New Delhi
Student-Roll: Alice -> 101
Product-Price: Laptop -> 45999.99

Student name: Alice
Roll number: 101

Explanation:

  • <K, V> - Two type parameters (Key and Value)
  • Can use different types for each parameter
  • Similar to how HashMap works internally
  • Provides flexibility to create complex data structures

Bounded Type Parameters

1. Upper Bounded (extends)

Restricts type parameter to be a specific class or its subclasses.

Syntax: <T extends Number> - T must be Number or its subclass (Integer, Double, etc.)

2. Multiple Bounds

Type parameter can have multiple bounds using & operator.

Syntax: <T extends Number & Comparable<T>> - T must be both Number and Comparable

3. Why Use Bounds?

Bounds allow you to call specific methods on the type parameter.

Example: If T extends Number, you can call doubleValue(), intValue(), etc.

Bounded Type Parameters Example

Let's create a calculator that only works with numeric types:

Example: Bounded Generic Calculator

// Generic class with upper bound
class Calculator {
    
    public double add(T num1, T num2) {
        return num1.doubleValue() + num2.doubleValue();
    }
    
    public double multiply(T num1, T num2) {
        return num1.doubleValue() * num2.doubleValue();
    }
    
    public boolean isGreater(T num1, T num2) {
        return num1.doubleValue() > num2.doubleValue();
    }
}

public class BoundedTypeExample {
    public static void main(String[] args) {
        
        // Calculator for Integer
        Calculator intCalc = new Calculator<>();
        System.out.println("Integer Calculator:");
        System.out.println("5 + 3 = " + intCalc.add(5, 3));
        System.out.println("5 ร— 3 = " + intCalc.multiply(5, 3));
        System.out.println("5 > 3? " + intCalc.isGreater(5, 3));
        
        // Calculator for Double
        Calculator doubleCalc = new Calculator<>();
        System.out.println("\nDouble Calculator:");
        System.out.println("5.5 + 2.3 = " + doubleCalc.add(5.5, 2.3));
        System.out.println("5.5 ร— 2.3 = " + doubleCalc.multiply(5.5, 2.3));
        System.out.println("5.5 > 2.3? " + doubleCalc.isGreater(5.5, 2.3));
        
        // This won't work - String is not a Number
        // Calculator strCalc = new Calculator<>();  // Error!
    }
}
Output:
Integer Calculator:
5 + 3 = 8.0
5 ร— 3 = 15.0
5 > 3? true

Double Calculator:
5.5 + 2.3 = 7.8
5.5 ร— 2.3 = 12.65
5.5 > 2.3? true

Explanation:

  • <T extends Number> - T must be Number or its subclass
  • Can call Number methods like doubleValue() on T
  • Works with Integer, Double, Float, Long, etc.
  • Prevents using non-numeric types like String

Wildcards in Generics

Wildcards provide flexibility when you don't know or care about the exact type:

Example: Using Wildcards

import java.util.ArrayList;
import java.util.List;

public class WildcardExample {
    
    // Unbounded wildcard - accepts any type
    public static void printList(List list) {
        System.out.print("[ ");
        for (Object obj : list) {
            System.out.print(obj + " ");
        }
        System.out.println("]");
    }
    
    // Upper bounded wildcard - Number or its subclasses
    public static double sumNumbers(List list) {
        double sum = 0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }
    
    // Lower bounded wildcard - Integer or its superclasses
    public static void addIntegers(List list) {
        list.add(10);
        list.add(20);
        list.add(30);
    }
    
    public static void main(String[] args) {
        
        // Unbounded wildcard example
        List stringList = List.of("Apple", "Banana", "Cherry");
        List intList = List.of(1, 2, 3, 4, 5);
        
        System.out.println("Unbounded Wildcard (?):");
        printList(stringList);
        printList(intList);
        
        // Upper bounded wildcard example
        List numbers1 = List.of(10, 20, 30);
        List numbers2 = List.of(1.5, 2.5, 3.5);
        
        System.out.println("\nUpper Bounded (? extends Number):");
        System.out.println("Sum of integers: " + sumNumbers(numbers1));
        System.out.println("Sum of doubles: " + sumNumbers(numbers2));
        
        // Lower bounded wildcard example
        List numList = new ArrayList<>();
        System.out.println("\nLower Bounded (? super Integer):");
        addIntegers(numList);
        System.out.println("After adding integers: " + numList);
    }
}
Output:
Unbounded Wildcard (?):
[ Apple Banana Cherry ]
[ 1 2 3 4 5 ]

Upper Bounded (? extends Number):
Sum of integers: 60.0
Sum of doubles: 7.5

Lower Bounded (? super Integer):
After adding integers: [10, 20, 30]

Explanation:

  • <?> - Unbounded wildcard, accepts any type (read-only)
  • <? extends Number> - Upper bound, accepts Number and its subclasses
  • <? super Integer> - Lower bound, accepts Integer and its superclasses
  • Use wildcards when you want flexibility with types

Benefits of Using Generics

Understanding the advantages of using Generics in your Java programs:

  • Type Safety: Errors caught at compile-time instead of runtime
  • No Casting: Eliminates tedious and error-prone type casting
  • Code Reusability: Write generic code once, use with any type
  • Better Performance: No runtime type checking or casting overhead
  • Cleaner Code: More readable and maintainable code
  • IDE Support: Better code completion and compile-time checking

๐ŸŽฏ Best Practice: Always use Generics with collections (ArrayList<String> instead of ArrayList). This prevents runtime errors and makes your code safer and more maintainable.

Important Points to Remember

Keep these key points in mind when working with Generics:

Key Generics Rules

Rule                           Description
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
No Primitives              Can't use int, double, etc. Use Integer, Double instead
Type Erasure               Generic type info is removed at runtime
No Static Generic Fields   Static fields can't use class type parameters
No Array Creation          Can't create arrays of generic types directly
Diamond Operator <>        Can omit type on right side: List<String> list = new ArrayList<>();

โš ๏ธ Common Mistake: You cannot use primitive types with Generics. Instead of ArrayList<int>, use ArrayList<Integer>. Java's autoboxing handles the conversion automatically!