January 25, 2023

Java Generics- Generic Class, Interface And Method

Generics in Java was introduced in Java 5 to provide strict type checking at compile time.

Type parameters in Java Generics

Generics in Java enables you to write generic classes, interfaces and methods that can work with different data types. It is possible because you specify type parameters when defining classes, interfaces and methods. Type parameter can be any class or interface like Integer, String, custom class or interface.

For example in Collection API ArrayList class is written as-

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

..
..
}

Here E is a type parameter which is enclosed in angle brackets (<>). Since a type parameter is specified- ArrayList<E> that means ArrayList class is a generic class where E is the type of elements in this List. With this generic class definition ArrayList can work with different data types. You will have to specify the actual type while initializing the ArrayList.

//List that stores Integers
List<Integer> nList = new ArrayList<Integer>();
nList.add(1);
nList.add(2);
nList.add(3);

// List that stores Strings
List<String> sList = new ArrayList<String>();
sList.add("A");
sList.add("B");
sList.add("C");

// List that stores objects of type Employee
List<Employee> eList = new ArrayList<Employee>();
Employee emp1 = new Employee("Jean", "HR", 6000);
eList.add(emp1);

Why Generics is required

You may argue that there is already an Object class in Java that can be used to refer any class object making it a good candidate to be used as a generic parameter. For example in the following class there is a method that has Object type as a parameter so you can pass any type to this method.

public class Test {
  public static void main(String[] args) throws IOException {
    Test t = new Test();
    t.display(1);
    t.display("Hello");
    t.display(5.67);
  }

  public void display(Object o) {
    System.out.println("passed argument is- " + o);
    System.out.println("passed argument's type is- " + o.getClass().getTypeName());
  }
}
Output
passed argument is- 1
passed argument's type is- java.lang.Integer
passed argument is- Hello
passed argument's type is- java.lang.String
passed argument is- 5.67
passed argument's type is- java.lang.Double

As you can see by using Object as parameter I can have a generic method that can work with any type so why Generics is required. Answer is Generics in Java bring type safety to your code. We’ll discuss that feature in next section.

Benefits of Java Generics

1. Strict type checks at compile time

Generics provide strict type checks at compile time so any type violation will give error as compile time itself rather than java.lang.ClassCastException thrown at run time.

For example you have initialized a List as a non-generic List and your intention is to store strings in it. Since it is not Generic which means all its elements will be stored as objects of Object class. If you add an Integer to this List by mistake there won't be any compile time error as Integer is also of type Object.

At the time of retrieving element from the List you will have to explicitly cast it to the type and that time it will throw ClassCastException when it encounters Integer.

public class Test {
  public static void main(String[] args) throws IOException {
    // Not generic
    List sList = new ArrayList();
    sList.add("A");
    sList.add("B");
    // Adding Integer
    sList.add(1);
    sList.add("C");
    
    Iterator itr = sList.iterator();
    while(itr.hasNext()){
      // Casting to string when retrieving
      String str = (String)itr.next();
      System.out.println("" + str);
    }
  }
}
Output
A
B
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at com.knpcode.programs.Test.main(Test.java:27)

With Generics you can specify the type of elements that can be stored in the List thus providing type safety. If you try to add element of any other type to this List you will get error at compile time itself.

public class Test {
  public static void main(String[] args) throws IOException {
    // Generic List
    List<String> sList = new ArrayList<String>();
    sList.add("A");
    sList.add("B");
    // Not allowed, Error at compile time if 
    // Integer is added 
    //sList.add(1);
    sList.add("C");
    
    Iterator<String> itr = sList.iterator();
    while(itr.hasNext()){
      String str = itr.next();
      System.out.println("" + str);
    }
  }
}

2. Explicit casting not required

Since type is specified with Generics and it is ensured that you can store elements only of the specified type so explicit casting is not required when retrieving elements.

In the above code when using a non-generic List, type casting is required.

Iterator itr = sList.iterator();
while(itr.hasNext()){
  // Casting to string when retrieving
  String str = (String)itr.next();
  System.out.println("" + str);
}

With a generic List type casting is not required.

Iterator<String> itr = sList.iterator();
while(itr.hasNext()){
  String str = itr.next();
  System.out.println("" + str);
}

3. Implement generic algorithms

By using generics, programmers can implement generic algorithms that work on different types, easier to read and are type safe too. Here is a simple Generic class example that can set and get value of any type.

public class Test<T> {
  T obj;
  public T getObj() {
    return obj;
  }
  public void setObj(T obj) {
    this.obj = obj;
  } 

  public static void main(String[] args) throws IOException {
    // With Integer type
    Test<Integer> intParam = new Test<Integer>();
    intParam.setObj(7);
    int value = intParam.getObj();
    System.out.println("Integer value- " + value);
    
    // With String type
    Test<String> strParam = new Test<String>();
    strParam.setObj("Test Value");
    String str = strParam.getObj();
    System.out.println("String value- " + str);
    
    // With Double type
    Test<Double> doubleParam = new Test<Double>();
    doubleParam.setObj(23.45);
    double dblValue = doubleParam.getObj();
    System.out.println("Double value- " + dblValue);
  }
}
Integer value- 7
String value- Test Value
Double value- 23.45

See How to write a generic Bubble Sort in Java in this post- Generic Bubble Sort Java Program

Type Parameter Naming Conventions in Java Generics

By convention, type parameter names are single, uppercase letters. The most commonly used type parameter names are:

  • T - Type
  • V - Value
  • E - Element
  • K - Key
  • N - Number
  • S,U,V etc. - 2nd, 3rd, 4th types

Generic Class

With the introduction of Generics done let’s see how we can create a Generic class in Java.

A generic class is defined with the following format:

class name<T1, T2, ..., Tn> { /* ... */ }

After the class name there is a type parameter section, delimited by angle brackets (<>). It specifies the type parameters (also called type variables) T1, T2, ..., and Tn.

Generic class creation Java example

In this example we'll create a generic class with two type parameters and use it with different data types.

class GenericClass<K, V> {
  private K key;
  private V value;
  public GenericClass(K key, V value) {
    this.key = key;
    this.value = value;
  }
  public K getKey(){
    return key;
  }
  public V getValue(){
    return value;
  }
}

public class GenericDemo{
  public static void main(String[] args) {
    GenericClass<String, String> g1 = new GenericClass<>("Test", "Value");
    System.out.println("Key- " + g1.getKey());
    System.out.println("Value- " + g1.getValue());

    GenericClass<Integer, Integer> g2 = new GenericClass<>(1, 2);
    System.out.println("Key- " + g2.getKey());
    System.out.println("Value- " + g2.getValue());
    
    GenericClass<Integer, String> g3 = new GenericClass<>(1, "One");
    System.out.println("Key- " + g3.getKey());
    System.out.println("Value- " + g3.getValue());
  }    
}
Output
Key- Test
Value- Value
Key- 1
Value- 2
Key- 1
Value- One

Generic Interface

A Generic interface is created just like Generic class.

interface name<T1, T2, ..., Tn> { /* ... */ }

Some of the rules that are to be followed while implementing a Generic Interface are as given below

  1. If generic type parameter is used with the interface then the class that implements a generic interface has to be a generic class with the same type parameter.
    public class GenericClass<E> implements GenericInterface<E>
    
  2. If you provide a data type with the Interface then you can use a normal class.
    public class NormalClass implements GenericInterface<Integer>
    
  3. A Generic class can have other parameters too apart from the type parameter it has to use because if implementing a generic interface.
    public class GenericClass<K, V, E> implements GenericInterface<E>
    

Generic Method

Any method in Generic class can specify the type parameters of the class and free to add type parameters of its own too. You can have a generic method in a non-generic class too.

Generic Method Java example

Here we’ll have a generic method in a non-generic class.

class TestClass {
  // Generic method
  public <T> void displayArrayElements(T[] arr){
    System.out.println("Elements in Array- " + Arrays.toString(arr));
  }
}

public class GenericDemo{
  public static void main(String[] args) {
    TestClass obj = new TestClass();
    Integer[] intArray = {1, 2, 3, 4, 5, 6, 7};
    Double[] doubleArray = {1.2, 2.3, 3.4, 4.5, 5.6};
    String[] strArray = {"A", "B", "C", "D"};
    obj.displayArrayElements(intArray);
    obj.displayArrayElements(doubleArray);
    obj.displayArrayElements(strArray);
  }    
}
Output
Elements in Array- [1, 2, 3, 4, 5, 6, 7]
Elements in Array- [1.2, 2.3, 3.4, 4.5, 5.6]
Elements in Array- [A, B, C, D]

As you can see if you are writing a generic method with its own type parameters then you need to declare type parameters after the access modifier.

public <T> void displayArrayElements(T[] arr)

You can also specify the actual data type in angular brackets when calling a generic method. Though Java can automatically infer type based on the type of the method arguments so doing that is not mandatory.

obj.displayArrayElements(intArray);
Or this
obj.<Integer>displayArrayElements(intArray);

The Diamond Operator

Java 7 onward it is not mandatory to specify the type arguments required to invoke the constructor of a generic class, you can pass an empty set of type arguments (<>) as long as the compiler can determine, or infer, the type arguments from the context. This pair of angle brackets, <>, is informally called the diamond.

For example, If you have a Generic class defined as given below

public class Test<T> {
  ..
  ..
}

Then you can create its instance like this Java 7 onward.

Test<Integer> obj = new Test<>();

No need to specify Integer on the right hand side, just pass the empty angle brackets <>, type will be inferred automatically.

That's all for the topic Java Generics- Generic Class, Interface And Method. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment