Java Generics are a powerful feature introduced in Java 5 that allows you to define classes, interfaces, and methods with a placeholder for the type of data they operate on. This enhances code reusability, type safety, and readability.
Generics enable you to write more generalized code that can operate on different data types while ensuring type safety at compile time.
Type Safety: Generics provide compile-time type checking, which reduces runtime errors. This ensures that you can catch type-related errors during compilation rather than at runtime.
Code Reusability: You can create a single class or method that works with different data types, reducing code duplication.
Elimination of Casting: When using generics, there’s no need for explicit type casting, which simplifies the code and reduces the risk of ClassCastException
.
Improved Readability: Generics make the code more self-documenting. By specifying types, the intention of the code becomes clearer.
Generics are implemented using type parameters, which are represented by angle brackets (<>
). You can define one or more type parameters for classes, interfaces, or methods.
The most commonly used type parameter names are:
T
: TypeE
: Element (typically used in collections)K
: KeyV
: ValueThe syntax for declaring a generic class or interface is as follows:
class ClassName {
// Use T as a type parameter
}
For methods, the syntax is:
public void methodName(T param) {
// Method implementation
}
Let’s start by creating a generic class. A common example is a simple box that can hold any type of object.
// Generic Box class
class Box {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
// Usage of Generic Box
public class Main {
public static void main(String[] args) {
Box stringBox = new Box<>();
stringBox.setItem("Hello, Generics!");
System.out.println(stringBox.getItem());
Box integerBox = new Box<>();
integerBox.setItem(123);
System.out.println(integerBox.getItem());
}
}
Box Class: The Box
class is defined with a type parameter T
. This allows it to store an item of any type.
Set and Get Methods: The setItem
and getItem
methods allow you to set and retrieve the item from the box, respectively.
Usage: In the Main
class, we create two instances of Box
: one for String
and another for Integer
. This demonstrates how the same class can handle different data types.
You can also create methods that utilize generics, regardless of whether the enclosing class is generic.
public class GenericMethodExample {
// Generic method to print an array of any type
public static void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"One", "Two", "Three"};
System.out.println("Integer Array:");
printArray(intArray);
System.out.println("String Array:");
printArray(strArray);
}
}
Generic Method Declaration: The printArray
method is declared with a type parameter <T>
. This allows it to accept an array of any type.
Method Usage: In the main
method, we call printArray
with both an Integer
array and a String
array, demonstrating the versatility of generics.
Sometimes you may want to restrict the types that can be used as type arguments. You can do this using bounded type parameters.
// Generic class with bounded type parameter
class NumberBox {
private T number;
public void setNumber(T number) {
this.number = number;
}
public double doubleValue() {
return number.doubleValue();
}
}
public class BoundedTypeExample {
public static void main(String[] args) {
NumberBox intBox = new NumberBox<>();
intBox.setNumber(10);
System.out.println("Double Value: " + intBox.doubleValue());
NumberBox doubleBox = new NumberBox<>();
doubleBox.setNumber(5.5);
System.out.println("Double Value: " + doubleBox.doubleValue());
}
}
Bounded Type Parameter: The NumberBox
class has a bounded type parameter <T extends Number>
, meaning it can only accept types that are subclasses of Number
(e.g., Integer
, Double
, etc.).
Method: The doubleValue
method calls doubleValue()
on the Number
type, ensuring it works with any subclass of Number
.
You can also specify multiple bounds for a type parameter using the &
symbol. This is useful when you want a type to inherit from multiple interfaces.
interface Readable {
void read();
}
interface Writable {
void write();
}
// Generic class with multiple bounds
class FileHandler {
private T file;
public FileHandler(T file) {
this.file = file;
}
public void processFile() {
file.read();
file.write();
}
}
// Example class implementing Readable and Writable
class TextFile implements Readable, Writable {
@Override
public void read() {
System.out.println("Reading text file.");
}
@Override
public void write() {
System.out.println("Writing to text file.");
}
}
public class MultipleBoundsExample {
public static void main(String[] args) {
TextFile textFile = new TextFile();
FileHandler fileHandler = new FileHandler<>(textFile);
fileHandler.processFile();
}
}
Interfaces: Readable
and Writable
interfaces define read
and write
methods.
Multiple Bounds: The FileHandler
class has a type parameter with multiple bounds, ensuring it can only accept types that implement both interfaces.
Implementation: The TextFile
class implements both interfaces, allowing it to be used with FileHandler
.
Wildcards are a special type of generics that allow for more flexible type parameters. The wildcard is represented by a question mark (?
).
import java.util.Arrays;
import java.util.List;
// Method to print list of numbers using wildcards
public class WildcardExample {
public static void printList(List extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List intList = Arrays.asList(1, 2, 3);
List doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Integer List:");
printList(intList);
System.out.println("Double List:");
printList(doubleList);
}
}
Wildcard Usage: The printList
method accepts a List<? extends Number>
, allowing it to handle a list of any subclass of Number
.
Flexibility: This makes the method flexible, as it can work with lists of different numeric types without requiring explicit type parameters.
While generics provide many advantages, they also come with limitations:
Type Erasure: Generics are implemented using type erasure, meaning the generic type information is not available at runtime. For example, you cannot create instances of type parameters (new T()
is not allowed).
Static Context: You cannot use type parameters in static contexts (e.g., static fields or static methods) since they are associated with instances of the class.
Primitive Types: Generics cannot be used with primitive types directly (e.g., int
, char
). Instead, you must use their wrapper classes (e.g., Integer
, Character
).
Let’s implement a simple generic stack data structure to further illustrate the use of generics.
import java.util.EmptyStackException;
// Generic Stack class
class GenericStack {
private Object[] elements;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public GenericStack() {
elements = new Object[DEFAULT_CAPACITY];
size = 0;
}
public void push(T item) {
if (size == elements.length) {
resize();
}
elements[size++] = item;
}
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
T item = (T) elements[--size];
elements[size] = null; // Prevent memory leak
return item;
}
public boolean isEmpty() {
return size == 0;
}
private void resize() {
int newCapacity = elements.length * 2;
Object[] newArray = new Object[newCapacity];
System.arraycopy(elements, 0, newArray, 0, elements.length);
elements = newArray;
}
}
// Example usage of Generic Stack
public class StackExample {
public static void main(String[] args) {
GenericStack stack = new GenericStack<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("Popped item: " + stack.pop());
System.out.println("Popped item: " + stack.pop());
System.out.println("Is stack empty? " + stack.isEmpty());
}
}
GenericStack Class: The GenericStack
class uses a generic type T
to store items of any type.
Push and Pop Methods: The push
method adds an item to the stack, and the pop
method retrieves and removes the top item.
Dynamic Resizing: The stack resizes itself when the capacity is reached, ensuring efficient memory usage.
Example Usage: In the main
method, we create a stack for Integer
types and perform push and pop operations.