Java introduced Generics in J2SE 5.0 as a solution for handling type-safe objects. Generics enable the definition of classes, interfaces, and methods with placeholders for types, facilitating the development of adaptable and recyclable code elements that can operate with various data types while ensuring type safety during compilation. This feature enhances code reliability by identifying errors during compilation.
Prior to the introduction of generics, it was possible to store objects of any type in a collection, known as non-generic. However, with the incorporation of generics, Java requires programmers to store a particular type of objects.
Why Generics?
Generics address several key issues in Java programming:
- Type Safety: Type safety is a critical aspect of Java programming. Before generics, collections in Java could hold any type of objects, which meant that type errors could only be caught at runtime, not compile time. Lack of type safety could lead to ClassCastException errors when an object was cast to the wrong type.
- Code Reusability: Generics allow for the creation of more general and reusable code. By parameterizing types, we can write methods and classes that can operate on any type, reducing code duplication and increasing flexibility.
- Elimination of Casting: Generics reduce the need for explicit type casting, making the code cleaner and less error-prone. Without generics, developers often have to cast objects to the desired type, leading to cluttered code and potential runtime errors.
- Enabling Generic Algorithms: Generics enable the creation of generic algorithms that can work with any type, enhancing the versatility of code. It is particularly useful in collections and other data structures.
- Enhancing the Java Collections Framework: The introduction of generics significantly enhanced the Java Collections Framework, making it more powerful and type-safe. Collections can now be parameterized with specific types, ensuring type safety and reducing the risk of runtime errors.
- Supporting More Readable and Maintainable Code: Generics make the code more readable and maintainable by explicitly stating what types are being used. This clarity helps other developers understand the intended use of collections and methods, reducing the likelihood of errors.
- Facilitating Type Inference: Java's type inference mechanisms work well with generics, allowing the compiler to deduce the type parameters in many cases, which simplifies the code for the developer.
- Backward Compatibility: Generics in Java were designed with type erasure to ensure backward compatibility with older versions of Java. Type erasure means that generic type information is only available at compile time and is removed at runtime. It allows generic code to interoperate with legacy code that does not use generics.
Advantage of Java Generics
The benefits of using generics can be summarized into three main advantages:
1) Type safety in generics restricts the storage of only one type of objects, preventing the inclusion of other object types.
When not using Generics, it is possible to store objects of any type.
List list = new ArrayList();
list.add(10);
list.add("10");
With Generics, it is required to specify the type of object we need to store.
List<Integer> list = new ArrayList<Integer>();
list.add(10);
list.add("10");// compile-time error
2) No necessity for type casting: There is no requirement to perform typecasting on the object.
Before Generics, we need to type cast.
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);//typecasting
After Generics, we don't need to typecast the object.
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);
3) Compile-Time Verification: Errors are identified during compilation, preventing issues during program execution. It is advisable to address potential problems during compilation rather than runtime for a more robust programming approach.
List<String> list = new ArrayList<String>();
list.add("hello");
list.add(32);//Compile Time Error
Syntax to use generic collection
ClassOrInterface<Type>
Example to use Generics in Java
ArrayList<String>
Full Example of Generics in Java
In this scenario, we are utilizing the ArrayList class; however, any collection class like LinkedList, HashSet, TreeSet, HashMap, Comparator, etc., can be employed.
Example
import java.util.*;
class TestGenerics1{
public static void main(String args[]){
ArrayList<String> list=new ArrayList<String>();
list.add("rahul");
list.add("jai");
//list.add(32);//compile time error
String s=list.get(1);//type casting is not required
System.out.println("element is: "+s);
Iterator<String> itr=list.iterator();
while(itr.hasNext()){
System.out.println(itr.next());
}
}
}
Output:
element is: jai
rahul
jai
Example of Java Generics using Map
Next, we will work with map components utilizing generics. In this scenario, both a key and a value must be provided. This concept can be clarified through a straightforward illustration:
Example
import java.util.*;
class TestGenerics2{
public static void main(String args[]){
Map<Integer,String> map=new HashMap<Integer,String>();
map.put(1,"vijay");
map.put(4,"umesh");
map.put(2,"ankit");
//Now use Map.Entry for Set and Iterator
Set<Map.Entry<Integer,String>> set=map.entrySet();
Iterator<Map.Entry<Integer,String>> itr=set.iterator();
while(itr.hasNext()){
Map.Entry e=itr.next();//no need to typecast
System.out.println(e.getKey()+" "+e.getValue());
}
}}
Output
1 vijay
2 ankit
4 umesh
Generic Class
A class that has the ability to represent various types is commonly referred to as a generic class. In this context, we employ the T type parameter to instantiate a generic class of a particular type.
Here is a straightforward illustration demonstrating the process of creating and utilizing a generic class.
Creating a generic class:
class MyGen<T>{
T obj;
void add(T obj){this.obj=obj;}
T get(){return obj;}
}
The T type serves as a placeholder that can represent various types such as String, Integer, or Employee. The specified type for the class will be utilized for the storage and retrieval of data.
Using Generic Class:
Let's see the code to use the generic class.
Example
class TestGenerics3{
public static void main(String args[]){
MyGen<Integer> m=new MyGen<Integer>();
m.add(2);
//m.add("vivek");//Compile time error
System.out.println(m.get());
}}
Output
Type Parameters
The type parameters naming conventions are important to learn generics thoroughly. The common type parameters are as follows:
- T - Type
- E - Element
- K - Key
- N - Number
- V - Value
Generic Method
Similar to a generic class, it is possible to establish a generic method that can receive arguments of any type. The range of arguments is confined to the method in which it is defined, accommodating both static and non-static methods.
Let's examine a basic illustration showcasing a Java generic function for displaying elements within an array. In this context, the symbol E is utilized to represent the element.
Example
public class TestGenerics4{
public static < E > void printArray(E[] elements) {
for ( E element : elements){
System.out.println(element );
}
System.out.println();
}
public static void main( String args[] ) {
Integer[] intArray = { 10, 20, 30, 40, 50 };
Character[] charArray = { 'J', 'A', 'V', 'A', 'T','P','O','I','N','T' };
System.out.println( "Printing Integer Array" );
printArray( intArray );
System.out.println( "Printing Character Array" );
printArray( charArray );
}
}
Output
Printing Integer Array
10
20
30
40
50
Printing Character Array
J
A
V
A
T
P
O
I
N
T
Wildcard in Java Generics
The symbol "?" (question mark) is used as a placeholder to denote a wildcard element, indicating any type. When we use <? extends Number>, it signifies any subclass of Number, such as Integer, Float, or Double. This allows us to invoke methods from the Number class using an object of any subclass.
A wildcard can be applied as a parameter, field, return type, or local variable, but it should not be utilized as a type argument for invoking a generic method, creating an instance of a generic class, or as a supertype.
Let's understand it by the example given below:
Example
import java.util.*;
// Abstract class representing a Shape with an abstract draw method
abstract class Shape {
abstract void draw();
}
// Class representing a Rectangle, subclass of Shape
class Rectangle extends Shape {
void draw() {
System.out.println("drawing rectangle");
}
}
// Class representing a Circle, subclass of Shape
class Circle extends Shape {
void draw() {
System.out.println("drawing circle");
}
}
// Class containing a generic method to draw shapes
class GenericTest {
// Generic method that accepts a list of any type that extends Shape
public static void drawShapes(List<? extends Shape> lists) {
// Loop through each Shape in the list and call its draw method
for (Shape s : lists) {
s.draw(); // Calling the draw method of the Shape class, which is implemented by the child class
}
}
public static void main(String args[]) {
// Creating a list of Rectangle objects
List<Rectangle> list1 = new ArrayList<Rectangle>();
list1.add(new Rectangle());
// Creating a list of Circle objects
List<Circle> list2 = new ArrayList<Circle>();
list2.add(new Circle());
list2.add(new Circle());
// Calling drawShapes method with the list of Rectangle objects
drawShapes(list1);
// Calling drawShapes method with the list of Circle objects
drawShapes(list2);
}
}
Output
drawing rectangle
drawing circle
drawing circle
Upper Bounded Wildcards
The main goal of upper bounded wildcards is to relax the constraints on a variable by allowing the unknown type to be a particular type or a subtype of that type. This is achieved by specifying a wildcard character ("?") along with the extends keyword for classes or the implements keyword for interfaces, followed by the upper bound of the type.
Syntax
List<? extends Number>
Here,
? is a wildcard character.
extends , is a keyword.
Number , is a class present in java.lang package
Suppose, we want to write the method for the list of Number and its subtypes (like Integer, Double). Using List<? extends Number> is suitable for a list of type Number or any of its subclasses whereas List<Number> works with the list of type Number only. So, List<? extends Number> is less restrictive than List<Number> .
Example of Upper Bound Wildcard
In this instance, we are employing upper bound wildcards to implement the method for List<Integer> and List<Double>.
Example
import java.util.ArrayList;
public class UpperBoundWildcard {
private static Double add(ArrayList<? extends Number> num) {
double sum=0.0;
for(Number n:num)
{
sum = sum+n.doubleValue();
}
return sum;
}
public static void main(String[] args) {
ArrayList<Integer> l1=new ArrayList<Integer>();
l1.add(10);
l1.add(20);
System.out.println("displaying the sum= "+add(l1));
ArrayList<Double> l2=new ArrayList<Double>();
l2.add(30.0);
l2.add(40.0);
System.out.println("displaying the sum= "+add(l2));
}
}
Output
displaying the sum= 30.0
displaying the sum= 70.0
Unbounded Wildcards
The unbounded wildcard type represents the list of an unknown type such as List<?>. This approach can be useful in the following scenarios: -
- When the given method is implemented by using the functionality provided in the Object class.
- When the generic class contains the methods that don't depend on the type parameter.
Example of Unbounded Wildcards
Example
import java.util.Arrays;
import java.util.List;
public class UnboundedWildcard {
public static void display(List<?> list)
{
for(Object o:list)
{
System.out.println(o);
}
}
public static void main(String[] args) {
List<Integer> l1=Arrays.asList(1,2,3);
System.out.println("displaying the Integer values");
display(l1);
List<String> l2=Arrays.asList("One","Two","Three");
System.out.println("displaying the String values");
display(l2);
}
}
Output
displaying the Integer values
1
2
3
displaying the String values
One
Two
Three
Lower Bounded Wildcards
Lower bounded wildcards serve the purpose of confining the unidentified type to a particular type or a type that is superior to that type. This is achieved by utilizing the wildcard character ("?") along with the keyword super, succeeded by the lower bound of the type.
Syntax
List<? super Integer>
Here,
? is a wildcard character.
super , is a keyword.
Integer , is a wrapper class.
Suppose, we want to write the method for the list of Integer and its supertype (like Number, Object). Using List<? super Integer> is suitable for a list of type Integer or any of its superclasses whereas List<Integer> works with the list of type Integer only. So, List<? super Integer> is less restrictive than List<Integer> .
Example of Lower Bound Wildcard
In this instance, we are utilizing lower bound wildcards to define the method for List<Integer> and List<Number>.
Example
import java.util.Arrays;
import java.util.List;
public class LowerBoundWildcard {
public static void addNumbers(List<? super Integer> list) {
for(Object n:list)
{
System.out.println(n);
}
}
public static void main(String[] args) {
List<Integer> l1=Arrays.asList(1,2,3);
System.out.println("displaying the Integer values");
addNumbers(l1);
List<Number> l2=Arrays.asList(1.0,2.0,3.0);
System.out.println("displaying the Number values");
addNumbers(l2);
}
}
Output
displaying the Integer values
1
2
3
displaying the Number values
1.0
2.0
3.0
Disadvantages of Java Generics
- Type Erasure: One of the fundamental limitations of Java Generics is type erasure. This design choice ensures backward compatibility with older versions of Java but introduces several issues:
- No Runtime Type Information: Because type information is erased at runtime, generic types cannot be used to obtain type-specific information. This limitation means you cannot directly check the type parameters of a generic instance at runtime using reflection.
Type Erasure and Type Safety: In some cases, type erasure may necessitate type casting, which has the potential to result in a ClassCastException during runtime if the type is mishandled.
public <T> T getFirstElement(List<T> list) {
return (T) list.get(0); // Potential ClassCastException due to type erasure
}
Complexity and Learning Curve
Introducing generics in Java introduces an additional level of intricacy, which may pose a learning curve for developers:
- Syntax Challenges: The syntax associated with generics can appear intricate and verbose, particularly when working with bounded type parameters, wildcards, and nested generic types. This intricacy has the potential to complicate code comprehension, especially for individuals new to programming.
- Debugging Difficulty: Debugging code that uses generics can be more challenging. Since type information is erased at runtime, the error messages related to type issues can be less informative and harder to trace back to the source.
- Advanced Features: Features like bounded wildcards (<? extends T> and <? super T>), generic methods and generic constructors can be difficult to master and correctly apply in practical scenarios.
public <T extends Comparable<? super T>> void sort(List<T> list) {
// Method signature with bounded type parameters and wildcards
}
Restrictions and Limitations
Generics have a set of limitations that may restrict their adaptability and effectiveness:
- Primitive Types Limitation: Java Generics are unable to work with primitive types directly. This necessitates the use of wrapper classes such as Integer and Double in place of int and double, potentially resulting in extra boxing and unboxing operations.
List<int> intList = new ArrayList<>(); // Compile-time error
List<Integer> integerList = new ArrayList<>(); // Correct usage