Generics are a highly practical tool in programming. They let us write code that works with multiple data types without duplicating it for each one. Think of them as a flexible template that can be adapted to different needs.
For example, suppose you need a function that sorts a list of numbers. Without generics, you might have to write separate functions for integers, floats, and so on. With generics, a single function handles sorting for all numeric types.
Background of generics in Java
When a collection container class is designed and declared, the actual type of the objects it will hold cannot be determined. So before JDK 1.5, element types could only be declared as Object, which meant a forced cast was needed when retrieving elements. Such casts are unsafe and can throw a ClassCastException at runtime.
With generics, you can specify the element type when declaring a collection. Type checking then happens at compile time, eliminating the runtime exceptions caused by forced casts and improving both safety and readability.
Generic method: public <T> void myMethod(T data) { ... }
Naming conventions for generics:
T: Type parameter
E: Element or Entry type
K, V: Key and Value types
Finally, again let’s take note of the naming convention used for the type parameters. We use T for type, whenever there isn’t anything more specific about the type to distinguish it. This is often the case in generic methods. If there are multiple type parameters, we might use letters that neighbor T in the alphabet, such as S. If a generic method appears inside a generic class, it’s a good idea to avoid using the same names for the type parameters of the method and class, to avoid confusion. The same applies to nested generic classes. Oracle Java Tutorials
JDK 7 introduced Type Inference, the compiler’s ability to infer generic type arguments from context. It makes code more concise and readable by avoiding repeated type declarations.
Type inference in Java generics refers to the compiler’s ability to deduce generic type arguments from context, making code more concise and readable while avoiding redundant type declarations.
Benefits of type inference:
Better readability: fewer redundant type arguments make code easier to understand and maintain.
Less code: no need to specify type arguments explicitly, simplifying the code you write.
More flexibility: code adapts to change more easily; for example, changing a method’s return type does not require updating type arguments at every call site.
The code below uses the raw type List, which is discouraged in modern Java. Raw types exist only for backward compatibility with pre-Java 5 code. They bypass generic type checking and can lead to a ClassCastException at runtime.
In Java generics, a wildcard is a symbol representing an unknown type. The common wildcards are ?, the upper bounded wildcard ? extends T, and the lower bounded wildcard ? super T.
Upper Bounds Wildcards ? extends T: ? can only be T or a subclass of T. After compilation, ? extends T is erased to T.
Lower Bounds Wildcards ? super T: ? can only be T or a superclass of T.
PECS (Producer Extends, Consumer Super) is an important guiding principle in Java generic programming, used to ensure type safety and correctness.
The PECS principle says: when a parameter produces data (Producer), use the upper bounded wildcard ? extends T; when a parameter consumes data (Consumer), use the lower bounded wildcard ? super T.
Producer Extends
Consumer Super
description
copy
source collection src
destination collection dest
public static <T> void copy(List<? super T> dest, List<? extends T> src)
sort
the list passed in
public static <T extends Comparable<? super T>> void sort(List<T> list)
compare
the comparator, which reads both arguments
public static <T> int compare(T a, T b, Comparator<? super T> c)
The Collections.copy() method is a good example of PECS in action. Among its parameters, src is the producer, used for reading data; dest is the consumer, used for writing data. So src uses the upper bounded wildcard ? extends T, and dest uses the lower bounded wildcard ? super T. This guarantees that the element type of dest is a supertype of the element type of src, preventing a ClassCastException.
A generic class differs from a non-generic class in that it declares type parameters. There can be multiple type parameters, separated by commas. Generic classes are commonly used to build general-purpose data structures, such as collection classes (ArrayList<T> e.g).
When writing a generic class, note that static methods cannot directly reference the class’s type parameter T. The reason is closely tied to the class loading mechanism and when type arguments are bound.
Why can’t static fields use type parameters?
A class’s static field is a class-level variable shared by all non-static objects of the class. Hence, static fields of type parameters are not allowed. Consider the following class:
Because the static field os is shared by phone, pager, and pc, what is the actual type of os? It cannot be Smartphone, Pager, and TabletPC at the same time. You cannot, therefore, create static fields of type parameters.
If a static member needs to work with generics, there are a few options:
Like generic classes, generic interfaces can declare type parameters. When a class implements a generic interface, it can either specify a concrete type argument or keep the type parameter generic.
A generic method declares its type parameters in angle brackets < > before the method’s return type.
Why use generic methods? A generic class must fix its type argument at instantiation; switching to another type requires a new instance, which can be inflexible. A generic method lets you specify the type at call time, which is more flexible.
The java.util.Collections class provides many generic methods for working with collections. For example, the sort method uses generics to sort a list of any type.
The java.util.Arrays class provides many generic methods for working with arrays. For example, the asList method uses generics to convert an array into a list.