Selft training repo
JDK 5.0 introduced Java Generics with the aim of reducing bugs and adding an extra layer of abstraction over types.
Generics in Java allow you to create classes, interfaces, and methods that can work with different data types while maintaining type safety. They enable you to write reusable code that can handle a variety of data types without sacrificing type checking at compile-time.
To create a generic class, you use angle brackets <>
and a placeholder type parameter.
The type parameter can be any valid identifier, but by convention, it’s often represented using a single uppercase letter. Inside the class, you can use this type parameter as if it were a regular data type.
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
You can also define generic methods within a non-generic class. To declare a generic method, you follow a similar syntax using angle brackets and type parameters before the return type of the method.
public class GenericUtils {
public static <T> void printArray(T[] array) {
for (T item : array) {
System.out.print(item + " ");
}
System.out.println();
}
}
Let’s demonstrate how to use the Box generic class and the printArray generic method
public class Main {
public static void main(String[] args) {
// Using the generic class
Box<Integer> integerBox = new Box<>(42);
System.out.println("Integer Box Content: " + integerBox.getContent());
Box<String> stringBox = new Box<>("Hello, Generics!");
System.out.println("String Box Content: " + stringBox.getContent());
// Using the generic method
Integer[] intArray = { 1, 2, 3, 4, 5 };
String[] stringArray = { "Java", "is", "awesome" };
System.out.print("Integer Array: ");
GenericUtils.printArray(intArray);
System.out.print("String Array: ");
GenericUtils.printArray(stringArray);
}
}
Output:
Integer Box Content: 42
String Box Content: Hello, Generics!
Integer Array: 1 2 3 4 5
String Array: Java is awesome
Initially, Java 1.5 introduced Generics, which enabled the parameterization of type arguments for classes, including Collections API. This feature eliminated the need to use raw types, which could potentially lead to casting exceptions at runtime.
Before:
List list = new ArrayList();
list.add("Java");
With generics:
List<String> list = new ArrayList<String>();
list.add("Java");
In Java 1.5, when declaring and constructing objects, we had to specify the parameterized type in the constructor, which could become cumbersome and hard to read. For instance:
Map<String, List<Map<String, Map<String, Integer>>>> cars = new HashMap<String, List<Map<String, Map<String, Integer>>>>();
The reason behind this approach was to maintain backward compatibility with raw types. The compiler needed to distinguish between raw types and generics. Here’s an example showing both types:
List<String> generics = new ArrayList<String>();
List<String> raws = new ArrayList();
Using raw types in constructors will prompt a warning message from the compiler, reminding us to parameterize the type:
ArrayList is a raw type. References to generic type ArrayList<E> should be parameterized
In Java 1.7, the Diamond Operator was introduced, which brought type inference and reduced assignment verbosity when using generics. With the diamond operator, the compiler infers the appropriate constructor declaration:
List<String> cars = new ArrayList<>();
This enhancement significantly improved code readability and made the usage of generics more concise.
Bounded type parameters allow you to restrict the types that can be used as generic arguments to a specific range of types.
There are two types of bounded type parameters: upper bounded and lower bounded.
An upper bounded type parameter is specified using the extends
keyword.
It restricts the generic type argument to be a specific type or any of its subclasses.
// Example: A generic class with an upper bounded type parameter
class Box<T extends Number> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Usage:
// Usage of the generic class with an upper bounded type parameter
Box<Integer> integerBox = new Box<>(42); // Valid, Integer extends Number
Box<Double> doubleBox = new Box<>(3.14); // Valid, Double extends Number
// Box<String> stringBox = new Box<>("Hello"); // Invalid, String does not extend Number
A lower bounded type parameter is specified using the super
keyword.
It restricts the generic type argument to be a specific type or any of its superclasses.
// Example: A generic method with a lower bounded type parameter
class Box<T> {
//...
public void setValue(List<? super T> list, T value) {
list.add(value);
}
}
Usage:
// Usage of the generic method with a lower bounded type parameter
List<Number> numberList = new ArrayList<>();
Box<Integer> integerBox = new Box<>();
integerBox.setValue(numberList, 42); // Valid, Integer is a subtype of Number
Box<Double> doubleBox = new Box<>();
doubleBox.setValue(numberList, 3.14); // Valid, Double is a subtype of Number
// Uncommenting the below lines would show that it is not allowed
// Box<String> stringBox = new Box<>();
// stringBox.setValue(numberList, "Hello"); // Invalid, String is not a subtype of Number
System.out.println(numberList); // Output: [42, 3.14]
Wildcards are denoted using the ?
symbol and are used to represent an unknown type. Wildcards are helpful when you want to define methods that operate on generic types without knowing the exact type.
Unbounded wildcards are denoted as <?>, and they represent an unknown type that can be anything.
// Example: A method that prints elements from a list using an unbounded wildcard
class ListPrinter {
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
}
Usage:
// Usage of the method with an unbounded wildcard
List<Integer> integerList = Arrays.asList(1, 2, 3);
List<String> stringList = Arrays.asList("A", "B", "C");
ListPrinter.printList(integerList); // Valid, List<Integer> matches List<?>
ListPrinter.printList(stringList); // Valid, List<String> matches List<?>
Bounded wildcards can be upper bounded or lower bounded, just like type parameters.
// Example: A method that calculates the sum of numbers in a list using an upper bounded wildcard
class MathUtils {
public static double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
}
Usage:
// Usage of the method with an upper bounded wildcard
List<Integer> integerList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
MathUtils.sumOfList(integerList); // Valid, List<Integer> extends List<? extends Number>
MathUtils.sumOfList(doubleList); // Valid, List<Double> extends List<? extends Number>
// MathUtils.sumOfList(stringList); // Invalid, List<String> does not extend Number
Generics were added to Java to ensure type safety. And to ensure that generics won’t cause overhead at runtime, the compiler applies a process called type erasure on generics at compile time.
Type erasure removes all type parameters and replaces them with their bounds or with Object if the type parameter is unbounded. This way, the bytecode after compilation contains only normal classes, interfaces and methods, ensuring that no new types are produced. Proper casting is applied as well to the Object type at compile time.
This is an example of type erasure:
public <T> List<T> genericMethod(List<T> list) {
return list.stream().collect(Collectors.toList());
}
With type erasure, the unbounded type T is replaced with Object:
// for illustration
public List<Object> withErasure(List<Object> list) {
return list.stream().collect(Collectors.toList());
}
// which in practice results in
public List withErasure(List list) {
return list.stream().collect(Collectors.toList());
}
If the type is bounded, the type will be replaced by the bound at compile time:
public <T extends Building> void genericMethod(T t) {
...
}
and would change after compilation:
public void genericMethod(Building t) {
...
}
https://www.baeldung.com/java-generics
Get Started |
Languages |
Java Development |
Java 5 |