Java Generics
Java Generics, introduced in Java 5, provide a powerful way to ensure type safety and to write flexible, reusable, and maintainable code. They allow you to define classes, interfaces, and methods with a placeholder for the type of data they operate on. This placeholder is replaced with an actual type when the code is used.
Generics help to eliminate the need for casting and ensure that you can work with types in a safe and clean manner. They are widely used in the Java Collections Framework and play a crucial role in modern Java programming.
Generics in Java allow you to write classes, interfaces, and methods that can operate on objects of various types while providing compile-time type safety. By using generics, you can eliminate the need for casting and ensure that the type you are working with is correct.
For example, a List
in Java can store objects of any type. But with generics, you can specify the type of object that the List
will store, providing more control and type safety.
A generic class is a class that can work with any type. You define a class with a type parameter (e.g., T
, E
, K
, V
) that is later replaced with a specific type when the class is instantiated.
// Generic class definition
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public class GenericClassExample {
public static void main(String[] args) {
// Using the Box class with Integer type
Box<Integer> integerBox = new Box<>();
integerBox.setValue(10);
System.out.println("Integer Value: " + integerBox.getValue()); // Output: 10
// Using the Box class with String type
Box<String> stringBox = new Box<>();
stringBox.setValue("Hello Generics");
System.out.println("String Value: " + stringBox.getValue()); // Output: Hello Generics
}
}
Explanation:
Box<T>
is a generic class where T
is a placeholder for the actual type.Box
, you specify the type (e.g., Integer
, String
), and the setValue
and getValue
methods use that type.Box
to hold different types without needing to create separate classes for each type.You can also define methods that use generics. The syntax for a generic method is similar to a generic class, but the type parameter is defined within the method.
public class GenericMethodExample {
// Generic method to print any type of object
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
public static void main(String[] args) {
// Using generic method with Integer array
Integer[] intArray = {1, 2, 3, 4, 5};
printArray(intArray);
// Using generic method with String array
String[] strArray = {"Java", "Generics", "Example"};
printArray(strArray);
}
}
Explanation:
printArray
takes an array of type T
and prints each element.<T>
before the return type specifies that this is a generic method and the actual type T
will be inferred based on the array passed.You can limit the types that can be used with generics by using bounded type parameters. This is useful when you want to restrict the types to subclasses of a particular class or implement a particular interface.
// Bounded type parameter example
class NumberBox<T extends Number> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public class BoundedTypeExample {
public static void main(String[] args) {
// Using the NumberBox class with Integer (which is a subclass of Number)
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setValue(10);
System.out.println("Integer Value: " + intBox.getValue()); // Output: 10
// Using the NumberBox class with Double (which is a subclass of Number)
NumberBox<Double> doubleBox = new NumberBox<>();
doubleBox.setValue(5.5);
System.out.println("Double Value: " + doubleBox.getValue()); // Output: 5.5
}
}
Explanation:
NumberBox<T>
restricts T
to types that extend Number
, so it can only work with Integer
, Double
, or other types that are subclasses of Number
.Java generics also support wildcards, which are represented by a question mark (?
). Wildcards allow you to define methods or classes that can accept various types without specifying the exact type.
?
)An unbounded wildcard can represent any type.
public class UnboundedWildcardExample {
// Method that accepts a list of any type
public static void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5);
List<String> strList = List.of("Java", "Generics", "Wildcard");
printList(intList);
printList(strList);
}
}
Explanation:
printList
accepts a list of any type (List<?>
), allowing it to print elements of any list, whether they contain Integer
, String
, or other types.? extends T
)An upper-bounded wildcard restricts the type to a subclass of a specified type.
public class UpperBoundedWildcardExample {
// Method that accepts a list of any type that extends Number
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4);
List<Double> doubleList = List.of(5.5, 6.6, 7.7);
printNumbers(intList);
printNumbers(doubleList);
}
}
Explanation:
printNumbers
accepts a list of any type that extends Number
. This allows the method to work with both Integer
and Double
lists.? super T
)A lower-bounded wildcard restricts the type to a superclass of a specified type.
public class LowerBoundedWildcardExample {
// Method that accepts a list of any type that is a superclass of Integer
public static void addNumbers(List<? super Integer> list) {
list.add(10); // Can add Integer to the list
list.add(20);
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // Output: [10, 20]
}
}
Explanation:
addNumbers
accepts a list of any type that is a superclass of Integer
. You can add Integer
objects to this list, but you cannot retrieve them as anything other than Object
.