Generics
Overview of Java Generics.
Java generics allows to write code that can work with different data types (String, Integer, etc., and user-defined types) without compromising on type safety. It is achieved by the concept of parameterized types.
What are generics?
Generics are basically templates for classes, interfaces, and methods.
These templates use type parameters, represented by letters like T or S, which act as placeholders for specific data types.
When using a generic class or method, provide the actual data type (e.g., Integer, String) to be used, which replaces the type parameter throughout the code.
The Object is the super-class of all other classes, and Object reference can refer to any object. These features lack type safety but Generics add that type of safety feature.
When we declare an instance of a generic type, the type argument passed to the type parameter must be a reference type and cannot be primitive data types like int, char.
Type Parameters naming convention
Type parameter names are single, uppercase letters. The common type parameters are as follows:
T – Type
E – Element (used extensively by the Java Collections Framework)
K – Key
N – Number
V – Value
Wildcard in Java Generics
The ? (question mark) symbol represents the wildcard element. It means any type. If we write <? extends Integer>
, it means any child class of String, e.g., Integer, Float, and double. Now we can call the method of Integer class through any child class object.
Wildcard can be used as a type of a parameter, field, return type, or local variable. However, it is not allowed to use a wildcard as a type argument for a generic method invocation, a generic class instance creation, or a super-type.
Upper Bounded Wildcards
It is used by declaring wildcard character ("?") followed by the extends keyword, followed by its upper bound
Example - ArrayList<? extends Number> num
public static <T extends Comparable> T maximum(T x, T y) {...}
Multiple Bounds - <T extends Number & Comparable>
Unbounded Wildcards
The unbounded wildcard type represents the list of an unknown type such as List<?>
Example - List<?> list
Lower Bounded Wildcards
It is used by declaring wildcard character ("?") followed by the super keyword, followed by its lower bound
Example - List<? super Integer> list
Benefits of using generics
Increased code re-usability: Single generic class/method can be written that can be used with different data types, reducing code duplication.
Improved type safety: The compiler takes care of type safety at compile time, preventing errors like mixing up different data types in the code.
Reduced casting: Generics eliminate the need for explicit type casting, making the code look cleaner and safer.
Type safety refers to a programming language ability to ensure that data is only used in ways that are consistent with its declared type. This helps prevent errors and unexpected behavior that can arise from mixing up different data types at runtime.
Type safety in Java:
Static typing: Java is a statically typed language, meaning the data type of a variable is declared at compile time and remains fixed throughout the program's execution.
Type checking: The Java compiler checks the types of expressions and operations to ensure they are compatible. For example, it wouldn't allow adding an integer and a string, as they have different data types.
Type conversion: Sometimes, controlled conversion between compatible types (e.g., casting an int to a double) might be necessary, but the compiler verifies its validity.
How generics contribute to type safety?
By declaring type parameters and requiring specific data types to be used with generics, the compiler can catch potential type mismatches early on. This prevents runtime errors like ClassCastException
, which occurs when trying to cast an object to an incompatible type at runtime. Generics promote writing code that is clear about the expected data types, making it easier to understand, maintain, and debug.
Types of Java generics
Generic classes:
How to Declare: Generic classes are blueprints/templates for creating objects with a specific data type. They declare one or more type parameters within angle brackets
< >
in the class declaration.
How to Use: Specify the actual data type when creating an instance
If the actual type argument is omitted, then the created object is a raw type of Box<T>:
Box is the raw type of the generic type Box<T>. However, a non-generic class or interface type is not a raw type. The warning shows that raw types bypass generic type checks, deferring the catch of unsafe code to runtime. Therefore, raw types should be avoided.
To completely disable unchecked warnings, use the -Xlint:-unchecked flag. The @SuppressWarnings("unchecked") annotation suppresses unchecked warnings.
Practical Use Case: A generic
Stack
class can work with different types of elements (e.g.,IntegerStack
,StringStack
).
Generic methods:
Similar to generic classes, these methods have type parameters in their signature, enabling them to operate on different data types. For instance, a swap(T a, T b) method can swap elements of any type.
How to Declare: Generic classes are blueprints/templates for creating objects with a specific data type. They declare one or more type parameters within angle brackets
< >
in the class declaration.
How to Use: Call the method with specific data types.
Practical Use Case: A generic
sort
method can be used to sort arrays of different data types (e.g.,sort(int[] arr)
,sort(String[] arr)
).
Generic interfaces:
These interfaces can also have type parameters, specifying the types of objects they can work with.
How to Declare: Similar to generic classes, generic interfaces define the behavior of objects using type parameters. They specify the types of objects that can implement the interface.
How to Use: Create concrete classes that implement the interface.
Practical Use Case: A generic
Map
interface can be used to create different types of maps (e.g.,HashMap<String, Integer>
,TreeMap<Integer, String>
).
Type Erasure
In Java, generics were added to ensure type safety and to ensure that generics won’t cause overhead at runtime, the compiler employs a process known as 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. With this, 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.
Examples
Generic Stack class
Generic Sort method
Generic Map Interface
While it's true that we can declare a generic array like private T[] elements;
within a generic class or method, there are limitations and caveats associated with generic arrays in Java.
Declaration: We can declare an array with a generic type parameter like
T[] elements;
within a generic class or method.Instantiation: However, you cannot directly instantiate an array of a generic type like
new T[10];
due to type erasure. This would result in a compile-time error.Workarounds: To work around this limitation, we can use type casting or other techniques. For example:
elements = (T[]) new Object[10];
elements = (T[]) Array.newInstance(clazz, size);
elements = (T[]) new ArrayList[size].toArray();
Type Safety: While we can use generic arrays, we may encounter issues related to type safety. Using arrays with generics can lead to unchecked warnings and potential runtime errors if the array elements are not properly cast.
Prefer Collections: Due to the complexities and limitations associated with generic arrays, it's generally recommended to use collections such as
ArrayList<T>
instead of arrays when working with generics in Java. Collections provide better type safety and flexibility compared to arrays.
Last updated