What is Generics?

Generics is a mechanism in programming that provides type safety and code reusability.

Generics allow defining classes, interfaces, and methods without specifying specific types, which are instead clarified when used. This approach offers several benefits:

  • Improved code reusability: By using generics, you can write a universal logic applicable to multiple different types without writing similar code for each type separately.
  • Enhanced type safety: It prevents type conversion errors at runtime. For example, if a method expects integer types, generics ensure that data of other types cannot be mistakenly passed in.

For instance, in Java, we can define a generic class Box<T> to represent a box that can store objects of any type:

class Box<T> {
    private T item;
 
    public void setItem(T item) {
        this.item = item;
    }
 
    public T getItem() {
        return item;
    }
}

In this example, the Box class is a generic class where T is a type parameter. T can be used as a variable type in the definition of the Box class. You can create instances of Box for different types, such as Box<Integer> for storing integers and Box<String> for storing strings.

Why Do We Need Generics?

Without generics, we might need to write corresponding classes or methods for each data type, leading to code redundancy and poor maintainability. With generics, we can pass types as parameters to classes or methods, making them versatile enough to handle multiple data types.

  • Executing the same code for multiple data types
private static int add(int a, int b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}
 
private static float add(float a, float b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}
 
private static double add(double a, double b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

Without generics, implementing addition for different types requires overloading an add method for each type. With generics, we can reuse a single method:

private static <T extends Number> double add(T a, T b) {
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}

Basic Syntax

The basic syntax of generics includes generic classes, generic interfaces, and generic methods.

Generic Class

public class Box<T> {
    private T content;
    public void setContent(T content) {
        this.content = content;
    }
    public T getContent() {
        return content;
    }
}

Generic Interface

public interface Container<T> {
    void add(T item);
    T get(int index);
}

Generic Method

public class Util {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

Usage Examples

Using a Generic Class

Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String content = stringBox.getContent();
System.out.println(content);

Using a Generic Method

Integer[] intArray = {1, 2, 3, 4, 5};
Util.printArray(intArray);
 
String[] strArray = {"A", "B", "C"};
Util.printArray(strArray);

That’s the basics! The following is an extension for further understanding.

Implementation Principle of Generics

Java’s generics are implemented using type erasure. This means that during the compilation phase, generic type information is erased and replaced with its upper-bound type (if no upper bound is specified, the default is Object).

For example, a generic type like List<String> is treated as List in the compiled bytecode. At runtime, specific generic type information does not actually exist.

This approach maintains backward compatibility of Java bytecode but also brings limitations, such as the inability to obtain specific type parameters of generics at runtime.

How to Write a Generic Class?

First, write the class for a specific type, such as String:

public class Pair {
    private String first;
    private String last;
    public Pair(String first, String last) {
        this.first = first;
        this.last = last;
    }
    public String getFirst() {
        return first;
    }
    public String getLast() {
        return last;
    }
}

Then, replace the specific type String with T and declare <T>:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

Type Parameter <T>

Type parameters are placeholders used when defining generic classes, interfaces, and methods, representing a certain type.

In generics, T in <T> is indeed customizable. Common type parameters include:

  • T: Represents Type.
  • E: Represents Element (for collections).
  • K and V: Represent Key and Value, respectively.
  • N: Represents Number.
  • S, U, V, etc.: Used for multiple type parameters.

However, these are conventional, not mandatory. You can use any valid letter or identifier as a generic parameter.

public class Pair<K, V> {
    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;
    }
}

In this example, the Pair class uses two type parameters, K and V, representing key and value respectively.

Although any letters can be used as type parameters, using meaningful letters (like T, E, K, V, etc.) improves code readability and understanding. For code involving multiple type parameters, it is recommended to choose meaningful letters to represent different types.

Example of Using Type Parameters

Define a generic class Box:

public class Box<T> {
    private T content;
 
    public void setContent(T content) {
        this.content = content;
    }
 
    public T getContent() {
        return content;
    }
}

Use this generic class:

Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String content = stringBox.getContent();

Wildcards <?>

Wildcards are placeholders used to represent unknown types when instantiating generic types, indicating an uncertain type. There are three common types:

  • Unbounded wildcard: <?>, representing any type.
  • Bounded wildcard (upper bound): <? extends T>, indicating the type must be T or a subclass of T.
  • Bounded wildcard (lower bound): <? super T>, indicating the type must be T or a superclass of T.

Example of Using Wildcards

Define a method to handle Box of any type:

public static void printBox(Box<?> box) {
    System.out.println(box.getContent());
}

Call this method:

Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
printBox(stringBox);
 
Box<Integer> intBox = new Box<>();
intBox.setContent(123);
printBox(intBox);

In this example, <?> is an unbounded wildcard, meaning the printBox method can accept Box of any type.

Example of Bounded Type Parameters

Sometimes, it is necessary to restrict type parameters, for example, allowing only a certain type and its subclasses:

public class NumberBox<T extends Number> {
    private T number;
 
    public void setNumber(T number) {
        this.number = number;
    }
 
    public T getNumber() {
        return number;
    }
}

In this example, T is restricted to Number and its subclasses.

Summary: Generics are a powerful feature in Java that improve code reusability and type safety through parameterized types. Understanding and using generics properly can significantly enhance code quality and maintainability.

Avatar

By BytePilot

Because sharing makes us better. Let’s learn, build, and grow — one byte at a time.

Leave a Reply

Your email address will not be published. Required fields are marked *