Java Generics

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.

Example

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.

Example

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.

Example

List<String> list = new ArrayList<String>();  

list.add("hello");  

list.add(32);//Compile Time Error

Syntax to use generic collection

Example

ClassOrInterface<Type>

Example to use Generics in Java

Example

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

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:

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

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

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:

Example

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

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

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

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

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

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

Example

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

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

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

    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

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

Example

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

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

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.

Example

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.
  • Example
    
    public <T extends Comparable<? super T>> void sort(List<T> list) {
    
        // Method signature with bounded type parameters and wildcards
    
    }
    
  • 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.
  • 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.
  • Example
    
    List<int> intList = new ArrayList<>(); // Compile-time error
    
    List<Integer> integerList = new ArrayList<>(); // Correct usage
    

Input Required

This code uses input(). Please provide values below: