Generics were introduced in Java 5 to provide a way to parameterize types. This allows for the creation of classes, interfaces, and methods where the type of data upon which they operate is specified as a parameter. The main goals of generics are to improve type safety and to enable code reuse.
Generics work only with Reference Types. When we declare an instance of a generic type, the type argument passed to the type parameter must be a reference type. We cannot use primitive data types like int, char. But primitive type arrays can be passed to the type parameter because arrays are reference types.
Benefits of Generics
- Type Safety: By using generics, you can detect errors at compile-time rather than at runtime. Generics enforce the type of objects that can be stored in a collection.
- Elimination of Casts: Generics reduce the need for casting. For instance, you do not need to cast an object retrieved from a collection to its type.
- Code Reusability: Generic methods and classes enable the same code to be used with different types.
Key Concepts
- Generic Classes and Interfaces: You can define classes and interfaces with type parameters.
- Generic Methods: Methods can be written with generic types.
- Bounded Type Parameters: You can restrict the types that can be used as type arguments.
- Type Inference: The compiler can often infer the type parameters from the context.
Generic Classes
You can define a generic class by specifying a type parameter in angle brackets <T>
.
// Generic class
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
System.out.println("Integer value: " + integerBox.get());
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generics");
System.out.println("String value: " + stringBox.get());
}
}
Generic Interfaces
Similar to generic classes, interfaces can also have type parameters.
// Generic interface
interface Pair<K, V> {
K getKey();
V getValue();
}
// Implementation of the generic interface
class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
public class GenericInterfaceExample {
public static void main(String[] args) {
Pair<String, Integer> pair = new OrderedPair<>("One", 1);
System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
}
}
// Generic interface
interface Pair<K, V> {
K getKey();
V getValue();
}
// Implementation of the generic interface
class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
public class GenericInterfaceExample {
public static void main(String[] args) {
Pair<String, Integer> pair = new OrderedPair<>("One", 1);
System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
}
}
Generic Methods
A method can be declared with type parameters, allowing it to operate on objects of various types.
public class GenericMethodExample {
// Generic method
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, 5 };
String[] stringArray = { "A", "B", "C", "D", "E" };
printArray(intArray);
printArray(stringArray);
}
}
Bounded Type Parameters
Bounded type parameters in generics allow you to restrict the types that can be used as arguments for a generic type. This is useful for imposing constraints on the type parameters, ensuring that they meet certain criteria (e.g., implementing a specific interface or extending a particular class).
Upper Bounded Type Parameters
Upper bounds restrict the types to be subtypes of a specified type. This is done using the extends
keyword.
public class BoundedTypeExample {
// Generic method with bounded type parameter
public static <T extends Number> void printNumber(T number) {
System.out.println("Number: " + number);
}
public static void main(String[] args) {
printNumber(10); // Integer
printNumber(10.5); // Double
// printNumber("10"); // Error: String is not a subtype of Number
}
}
// In this example, Box<T extends Number> ensures that the type parameter T must be a subclass of Number
Lower Bounded Type Parameters
Lower bounds restrict the types to be supertypes of a specified type. This is done using the super
keyword and is commonly used with wildcards.
import java.util.ArrayList;
import java.util.List;
public class LowerBoundExample {
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList);
List<Object> objectList = new ArrayList<>();
addNumbers(objectList);
System.out.println(objectList);
// List<Double> doubleList = new ArrayList<>();
// addNumbers(doubleList); // Compilation error: Double is not a supertype of Integer
}
}
Type Inference
Type inference is a Java feature that allows the compiler to automatically determine the type of a generic method or class based on the context in which it is used. This feature simplifies the code by reducing the need for explicit type declarations. This feature makes generic code more readable and reduces boilerplate code.
import java.util.ArrayList;
import java.util.List;
public class TypeInferenceExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(); // Type inference in constructor
list.add("Hello");
list.add("Generics");
for (String element : list) {
System.out.println(element);
}
}
}
//compiler infers the type of the generic parameter from the left-hand side of the assignment.
public class TypeInferenceExample {
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, 5};
String[] stringArray = {"A", "B", "C", "D"};
// Type inference in action: the compiler infers that T is Integer
printArray(intArray);
// Type inference in action: the compiler infers that T is String
printArray(stringArray);
}
}
// compiler infers the type T based on the type of the array passed to the method.
Wildcards
Generics also support wildcards, which represent an unknown type. Wildcards are useful in scenarios where you want to work with generic types but do not need to know the exact type.
- Unbounded Wildcards: Represented by
?
, it can hold any type. It is useful when the type is irrelevant.
import java.util.List;
public class UnboundedWildcardExample {
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<String> strList = List.of("A", "B", "C");
printList(intList);
printList(strList);
}
}
- Bounded Wildcards with Upper Bounds (
? extends T
) : An upper-bounded wildcard restricts the unknown type to be a specific type or a subtype of that type. It is useful for reading from a generic object.
import java.util.Arrays;
import java.util.List;
public class UpperBoundedWildcardExample {
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Sum of intList: " + sumOfList(intList));
System.out.println("Sum of doubleList: " + sumOfList(doubleList));
}
}
- Bounded Wildcards with Lower Bounds (
? super T
) : A lower-bounded wildcard restricts the unknown type to be a specific type or a supertype of that type.
import java.util.ArrayList;
import java.util.List;
public class LowerBoundedWildcardExample {
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println("Number list: " + numberList);
}
}