Introduction to Java Generics: Why Use Generics? Simple Understanding and Usage

Why Use Java Generics?

In Java, we often need to handle various data collections, such as storing student information or product details. Without generics, directly using the Object type to store data, while flexible, brings two main issues: type unsafety and cumbersome type casting.

For example, without generics, you might write:

// Collection without generics, storing Object type
List list = new ArrayList();
list.add("Zhang San");
list.add(20); // Accidentally added an integer type

// Casting is required when retrieving data, which is error-prone
String name = (String) list.get(0); // Correct
Integer age = (Integer) list.get(1); // Correct
String wrong = (String) list.get(1); // Runtime ClassCastException!

The problem here is that list can store any type of data. The compiler cannot check type matches in advance, and exceptions will be thrown at runtime if types mismatch.

What Are Generics?

Generics, introduced in Java 5, allow us to pass types as parameters to classes, interfaces, or methods, enabling type safety and code reuse. In simple terms, generics are “parameterized types”—for example, List<String> represents a list that can only store String types.

How to Use Generics?

1. Generic Classes

Generic classes are the most basic application of generics. Define them by declaring a type parameter (e.g., <T>) after the class name. T is a placeholder (customizable names like E, K, V are also common).

Example: Define a General “Box” Class

class Box<T> {
    private T content; // Use T as the type parameter

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }
}

Using the Generic Class

// Box storing Strings
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String str = stringBox.getContent(); // No casting needed, returns String directly

// Box storing Integers
Box<Integer> intBox = new Box<>();
intBox.setContent(100);
Integer num = intBox.getContent(); // Returns Integer directly

Generics allow the Box class to flexibly store any type of data. The compiler checks type matches at compile time, avoiding runtime errors.

2. Generic Interfaces

Generic interfaces are similar to generic classes. Declare type parameters after the interface name, and implementing classes must specify concrete types.

Example: Define a “Generator” Interface

interface Generator<T> {
    T generate(); // Interface method returns type T
}

// Implementing the generic interface (specify concrete type)
class StringGenerator implements Generator<String> {
    @Override
    public String generate() {
        return "Generic String";
    }
}

// Usage
Generator<String> generator = new StringGenerator();
String result = generator.generate(); // Returns "Generic String"

3. Generic Methods

Generic methods are methods that have their own type parameters, applicable in both ordinary and generic classes.

Example: Define a General “Print” Method

class Util {
    // Generic method: Accepts an array of any type, returns the first element
    public static <T> T getFirstElement(T[] array) {
        if (array.length == 0) return null;
        return array[0];
    }
}

// Usage
String[] strArray = {"A", "B", "C"};
String firstStr = Util.getFirstElement(strArray); // Returns "A"

Integer[] intArray = {1, 2, 3};
Integer firstInt = Util.getFirstElement(intArray); // Returns 1

4. Generic Collections (Most Common)

Java standard library collections (e.g., ArrayList, HashMap) support generics, a typical application scenario.

Example: Using Generic Collections

// ArrayList storing String types
List<String> names = new ArrayList<>();
names.add("Alice"); // Correct
// names.add(123); // Compilation error: Type mismatch, only String allowed
names.forEach(System.out::println); // Iterate and print: Alice

// HashMap storing key-value pairs (key: String, value: Integer)
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90); // Correct
scores.put("Bob", 85);
// scores.put("Charlie", "95"); // Compilation error: Value must be Integer
Integer aliceScore = scores.get("Alice"); // No casting needed, returns Integer directly

Wildcards: Making Generics More Flexible

The wildcard <?> handles the type range of generic collections, with two common forms:

  1. Upper Bounded Wildcard: <? extends T>
    Indicates elements in the collection can only be T or its subclasses (with T as the upper bound).
    Example: Allows storing Number and its subclasses (e.g., Integer, Double).
   List<? extends Number> numbers = new ArrayList<Integer>();
   numbers.add(new Integer(10)); // Error! Cannot add elements
   Number num = numbers.get(0); // Correct, returns Number type
  1. Lower Bounded Wildcard: <? super T>
    Indicates elements in the collection can only be T or its superclasses (with T as the lower bound).
    Example: Allows storing Integer and its superclasses (e.g., Number, Object).
   List<? super Integer> ints = new ArrayList<Object>();
   ints.add(new Integer(10)); // Correct
   Object obj = ints.get(0); // Correct, returns Object type

Core Advantages of Generics

  1. Type Safety: Type checks at compile time prevent ClassCastException at runtime.
  2. Eliminates Casting: No manual casting required, resulting in cleaner code.
  3. Code Reusability: Implement a single logic to handle multiple types via type parameters, avoiding redundant code.

Precautions

  1. Primitive Types Not Allowed: Generic types must be reference types (e.g., use Integer instead of int).
   List<int> nums = new ArrayList<>(); // Error! Use List<Integer> instead
  1. Generics Do Not Support Inheritance: List<String> and List<Object> are independent and cannot be assigned to each other.
   List<String> strList = new ArrayList<>();
   List<Object> objList = strList; // Error: Generics are not inheritable
  1. Type Erasure: Generic types are erased at runtime (e.g., T is replaced with Object). Thus, you cannot directly instantiate T (e.g., new T()).

Summary

Generics are a crucial Java feature for ensuring type safety and code reuse. By parameterizing types, they make collections and classes more flexible and secure. Mastering generics involves understanding type parameters, generic classes/methods/interfaces, and wildcards. Start with common collection generics, then gradually practice generic classes and methods to improve code quality.

Through the examples and explanations above, you should now understand the value and usage of Java generics. Generics simplify code and reduce errors, making it a powerful tool for writing robust Java programs!

Xiaoye