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
andV
: 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 beT
or a subclass ofT
. - Bounded wildcard (lower bound):
<? super T>
, indicating the type must beT
or a superclass ofT
.
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.