Java Generics


Java Generics, introduced in Java 5, provide a powerful way to ensure type safety and to write flexible, reusable, and maintainable code. They allow you to define classes, interfaces, and methods with a placeholder for the type of data they operate on. This placeholder is replaced with an actual type when the code is used.

Generics help to eliminate the need for casting and ensure that you can work with types in a safe and clean manner. They are widely used in the Java Collections Framework and play a crucial role in modern Java programming.


What Are Generics in Java?

Generics in Java allow you to write classes, interfaces, and methods that can operate on objects of various types while providing compile-time type safety. By using generics, you can eliminate the need for casting and ensure that the type you are working with is correct.

For example, a List in Java can store objects of any type. But with generics, you can specify the type of object that the List will store, providing more control and type safety.

Key Benefits of Generics

  1. Type Safety: Generics provide compile-time type checking, reducing the chance of runtime errors due to incorrect types.
  2. Elimination of Casts: By using generics, you eliminate the need for explicit casting, making the code cleaner and less error-prone.
  3. Code Reusability: Generics allow you to write flexible and reusable classes, interfaces, and methods that can work with any object type.

Basic Syntax of Java Generics

1. Generic Classes

A generic class is a class that can work with any type. You define a class with a type parameter (e.g., T, E, K, V) that is later replaced with a specific type when the class is instantiated.

Example: A Generic Class

// Generic class definition
class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        // Using the Box class with Integer type
        Box<Integer> integerBox = new Box<>();
        integerBox.setValue(10);
        System.out.println("Integer Value: " + integerBox.getValue());  // Output: 10
        
        // Using the Box class with String type
        Box<String> stringBox = new Box<>();
        stringBox.setValue("Hello Generics");
        System.out.println("String Value: " + stringBox.getValue());  // Output: Hello Generics
    }
}

Explanation:

  • The class Box<T> is a generic class where T is a placeholder for the actual type.
  • When creating an instance of Box, you specify the type (e.g., Integer, String), and the setValue and getValue methods use that type.
  • This allows Box to hold different types without needing to create separate classes for each type.

2. Generic Methods

You can also define methods that use generics. The syntax for a generic method is similar to a generic class, but the type parameter is defined within the method.

Example: A Generic Method

public class GenericMethodExample {
    // Generic method to print any type of object
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        // Using generic method with Integer array
        Integer[] intArray = {1, 2, 3, 4, 5};
        printArray(intArray);

        // Using generic method with String array
        String[] strArray = {"Java", "Generics", "Example"};
        printArray(strArray);
    }
}

Explanation:

  • The generic method printArray takes an array of type T and prints each element.
  • The <T> before the return type specifies that this is a generic method and the actual type T will be inferred based on the array passed.
  • This method works with arrays of any type, demonstrating the flexibility of generics.

Bounded Type Parameters

You can limit the types that can be used with generics by using bounded type parameters. This is useful when you want to restrict the types to subclasses of a particular class or implement a particular interface.

Example: Bounded Type Parameter

// Bounded type parameter example
class NumberBox<T extends Number> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class BoundedTypeExample {
    public static void main(String[] args) {
        // Using the NumberBox class with Integer (which is a subclass of Number)
        NumberBox<Integer> intBox = new NumberBox<>();
        intBox.setValue(10);
        System.out.println("Integer Value: " + intBox.getValue());  // Output: 10
        
        // Using the NumberBox class with Double (which is a subclass of Number)
        NumberBox<Double> doubleBox = new NumberBox<>();
        doubleBox.setValue(5.5);
        System.out.println("Double Value: " + doubleBox.getValue());  // Output: 5.5
    }
}

Explanation:

  • The class NumberBox<T> restricts T to types that extend Number, so it can only work with Integer, Double, or other types that are subclasses of Number.
  • This prevents the use of non-numeric types, ensuring type safety when dealing with numbers.

Wildcards in Generics

Java generics also support wildcards, which are represented by a question mark (?). Wildcards allow you to define methods or classes that can accept various types without specifying the exact type.

1. Unbounded Wildcard (?)

An unbounded wildcard can represent any type.

Example: Using an Unbounded Wildcard

public class UnboundedWildcardExample {
    // Method that accepts a list of any type
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4, 5);
        List<String> strList = List.of("Java", "Generics", "Wildcard");

        printList(intList);
        printList(strList);
    }
}

Explanation:

  • The method printList accepts a list of any type (List<?>), allowing it to print elements of any list, whether they contain Integer, String, or other types.

2. Upper Bounded Wildcard (? extends T)

An upper-bounded wildcard restricts the type to a subclass of a specified type.

Example: Using an Upper Bounded Wildcard

public class UpperBoundedWildcardExample {
    // Method that accepts a list of any type that extends Number
    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4);
        List<Double> doubleList = List.of(5.5, 6.6, 7.7);

        printNumbers(intList);
        printNumbers(doubleList);
    }
}

Explanation:

  • The method printNumbers accepts a list of any type that extends Number. This allows the method to work with both Integer and Double lists.

3. Lower Bounded Wildcard (? super T)

A lower-bounded wildcard restricts the type to a superclass of a specified type.

Example: Using a Lower Bounded Wildcard

public class LowerBoundedWildcardExample {
    // Method that accepts a list of any type that is a superclass of Integer
    public static void addNumbers(List<? super Integer> list) {
        list.add(10);  // Can add Integer to the list
        list.add(20);
    }

    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        addNumbers(numberList);
        System.out.println(numberList);  // Output: [10, 20]
    }
}

Explanation:

  • The method addNumbers accepts a list of any type that is a superclass of Integer. You can add Integer objects to this list, but you cannot retrieve them as anything other than Object.