Generics are one of Java’s most powerful features, introduced in Java 5. They allow developers to write type-safe, flexible, and reusable code by enabling classes, interfaces, and methods to operate on objects of various types, while still enforcing compile-time type checking. Mastering generics is essential for building robust, maintainable, and scalable applications in Java.
What Are Java Generics?
Generics allow you to write code that works with multiple types while preserving type safety. Without generics, developers often rely on Object references, which require casting and are prone to runtime errors like ClassCastException. Generics solve this problem by enabling compile-time type checking.
Key Benefits of Generics
- Type Safety: Eliminates explicit type casting and reduces runtime errors.
- Code Reusability: Enables writing classes and methods that work with multiple types.
- Compile-time Checking: Detects type mismatches during compilation, preventing subtle bugs.
- Improved Readability: Clearly communicates the intended types in collections and APIs.
The Basics: Generic Syntax
Generics are specified using angle brackets (< >). A type parameter (like T) acts as a placeholder for the actual type.
class MyClass<T> {
private T value;
public MyClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Here:
Tis a type parameter that will be replaced with an actual type at instantiation.MyClass<Integer>will store integers, whileMyClass<String>will store strings—without any need for type casting.
1. Generic Classes
A generic class allows a class to work with any data type, defined when creating an instance.
Example: Generic Class
public class Box<T> {
private T item;
public Box(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
public static void main(String[] args) {
Box<Integer> intBox = new Box<>(10);
System.out.println("Integer value: " + intBox.getItem());
Box<String> strBox = new Box<>("Hello Generics");
System.out.println("String value: " + strBox.getItem());
}
}
Explanation:
Box<T>defines a generic class.intBoxandstrBoxdemonstrate how the same class can handle different types safely.- No explicit casting is required, thanks to generics.
2. Generic Methods
You can define methods that are generic, independent of the class’s type. The type parameter appears before the return type.
Example: Generic Method
public class GenericMethodExample {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
String[] strArray = {"Java", "Generics", "Example"};
printArray(intArray);
printArray(strArray);
}
}
Explanation:
<T>beforevoiddeclares the method as generic.- Works with arrays of any type (
Integer,String, etc.). - Avoids code duplication while maintaining type safety.
3. Bounded Type Parameters
Sometimes, you want to restrict the types a generic can accept. This is done with bounded type parameters using extends.
Example: Upper-Bounded Generics
public class BoundedTypeExample {
public static <T extends Number> void printSquare(T number) {
System.out.println("Square of " + number + " is: " + (number.doubleValue() * number.doubleValue()));
}
public static void main(String[] args) {
printSquare(4); // Integer
printSquare(3.5); // Double
// printSquare("Hello"); // Compile-time error
}
}
Explanation:
<T extends Number>restrictsTtoNumberor its subclasses.- Prevents invalid types at compile time.
- Enables using
Numbermethods likedoubleValue().
Tip: You can also use interfaces in bounds:
<T extends Comparable<T>> void sort(T[] array) { ... }
4. Wildcards in Generics
Wildcards (?) represent unknown types and are useful for writing flexible, reusable code with collections.
Types of Wildcards
- Unbounded wildcard:
List<?>→ unknown type. - Upper-bounded wildcard:
List<? extends Number>→ any subclass ofNumber. - Lower-bounded wildcard:
List<? super Integer>→ any superclass ofInteger.
Example: Upper-Bounded Wildcard
import java.util.List;
public class WildcardExample {
public static void printNumbers(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.1, 2.2, 3.3);
printNumbers(ints);
printNumbers(doubles);
}
}
Explanation:
? extends Numberallows the method to accept any Number subclass.- Enables maximum flexibility without sacrificing type safety.
5. Generic Interfaces
Generics are not limited to classes and methods. Interfaces can also be generic, defining a type-safe contract for implementations.
Example: Generic Interface
interface Box<T> {
void setItem(T item);
T getItem();
}
class StringBox implements Box<String> {
private String item;
@Override
public void setItem(String item) { this.item = item; }
@Override
public String getItem() { return item; }
}
public class GenericInterfaceExample {
public static void main(String[] args) {
StringBox box = new StringBox();
box.setItem("Hello Generics");
System.out.println("Stored item: " + box.getItem());
}
}
Explanation:
Box<T>defines a generic interface.StringBoxprovides a concrete type (String), maintaining type safety.- Promotes flexible API design.
Best Practices for Using Generics
- Prefer generics over raw types: Avoid
Listwithout type parameters. - Use bounded types judiciously: Restrict types when specific behavior is needed.
- Leverage wildcards for API flexibility: Especially in method parameters.
- Keep code readable: Use descriptive names like
<T>,<E>,<K,V>consistently. - Avoid over-complicating: Only use generics when they provide real benefits.
Conclusion
Generics are a cornerstone of modern Java programming. They allow you to write type-safe, reusable, and maintainable code, while also improving readability and performance. By mastering:
- Generic classes
- Generic methods
- Bounded type parameters
- Wildcards
- Generic interfaces