generics

Java Generics Examples

1. Introduction

Sun Microsystems included Java Generics in java 1.5 to introduce the type-safety feature while using the collection classes. It also complements auto-boxing, auto-unboxing, bounded variables, covariance, etc. There are other benefits of Generics such as java generic methods. We will uncover each.

You can also check this tutorial in the following video:

Java Generics Tutorial – video

2. What is Java Generics

Java Generics is a technical term denoting a set of language features related to the definition and use of generic types and methods. In Java, generic types or Methods differ from regular types and methods in how they have type parameters associated with them. The idea is to allow type (Integers, Strings, and user-defined types) to be a parameter to methods, classes, and interfaces.

java generics

Java Generics is a way to specify concrete types to general-purpose classes and methods that operated on Object before. Java generics can be used with inbuilt classes, user-defined classes, methods, and interfaces. Let’s start some built-in classes, interfaces, and method available in the Java Collection framework-

We will take an example of the List class. In general, when we don’t use generics, the code looks like below-

Using List without generics

// Creating names without generics syntax
List names = new ArrayList();

// Adding an Integer
names.add(new Integer(75));

// Adding a String
names.add("This is a String");

// getting 0th element and explicitly typecasting into Integer
Integer integer = (Integer) names.get(0);

// getting 1st element and explicitly typecasting into String
String string = (String) names.get(1);

// getting 1st element and typecasting into int leads to ClassCastException
Integer integerByMistake = (Integer) names.get(1);

// getting 0th element without typecasting leads to Compile time error
Integer integer1 = names.get(0);

Explanation: In the above example, we created a List without using generic syntax which enables it to take any type of value i.e. It can accept any object. Further, we added a string and an integer into that list. Until this point, there were no problems. Now when we tried to get an element of the list by using get() method in the next lines below problems come into picture-

  • We have to explicitly typecast the values to the appropriate type which may lead to runtime exception if it is not convertible to target type.
  • No Type inference while getting values from the list.
  • Chances are there to add a wrong typecasting as shown in the last line of code which may lead to ClassCastException.

All the above problem began because there is no type-safety, autoboxing, and auto-unboxing of the elements. Java generics solves this problem. Let’s convert the above example in generics and see how it looks.

Using List with generics

// Creating names with generics syntax
List<String> names = new ArrayList<String>();

// Adding an Integer compile time error
names.add(new Integer(75));

// Adding a String
names.add("This is a String");

// getting 0th element and typecasting into Integer Compile time error
Integer integer = names.get(0);

// getting 1st element and typecasting into String without any error
String string = names.get(1);

Explanation: In the above code snippet, We converted the previous code to use generics and advantages of this code are-

  • We created a type-safe list of string.
  • It automatically detects the type when we try to get the values from the list which eliminated the explicit typecasting and avoid ClassCastException.
  • It prevents accidental addition of the wrong type of element into the list leading to a compile-time error.

3. Advantages of Java Generics

Following are the advantages of using generics in regular code practice-

  • Stronger type checks at compile-time. The Java compiler applies strong type-checking to the generic code and issues error if the code violates type-safety. Fixing compile-time errors are easier than fixing runtime errors because they are difficult to find.
  • Eliminates the cast by inferring the type from the declaration-statement.
  • Reusable Code, It enables programmers to implement generic algorithms by specifying type parameter in method and class and interfaces and reusing the same code for different types of Objects.

4. Type Inference with Diamond Operator

From Java 1.7 onwards, We can use diamond operator (<>) while instantiating a generic class. The Java compiler can infer the class-type to have the same type as the variable we assign it to. We can use Diamond operator in the same above code as follows-

Using List with diamond operator

// Creating names with generics and diamond operator
List<String> names = new ArrayList<>();
names.add("This is a String");
String string = names.get(0);

Explanation: In the above code, we used the diamond operator (<>) in the very 1st line to instantiate the List with String as a type.

5. Simplified For Loop with Java Generics

Java 1.5 got another good feature with the generics is a for-each loop which operates excellent with generic-types.

for each loop with generics

// Creating names with generics and diamond operator
List names = new ArrayList<>();
names.add("Jack");
names.add("John");
names.add("Rock");

// for-each loop with generic collection
for (String name : names)
    System.out.println(name);

Explanation: In the above code, we created a list of names and used the for-each loop to iterate and print the names in it. It removed the usage of Iterator, Iterator.hashNext() and Iterator.next() with normal while loop.

6. Flavors of Java Generics

We can use generics syntax at different places in our code based on that we can categorize generics in three different types-

  • Generic Type Class or Interface
  • Generic Type Method or Constructor
  • Generic Type Arrays

6.1 Generic Type Class or Interface

The Generic type classes and interfaces are also known as the row types because they don’t have an actual type associated. We have to pass the type explicitly by the type-parameter as an argument while constructing an instance of it.

Let’s understand it with an example, We wanted to create a class Container which contains anything like Cars, Balls, and etc. One way to do this is to create a class Container with a field of Object type as shown below-

class Container {
    private Object element;

    public Object getElement() {
        return element;
    }

    public void setElement(Object element) {
        this.element = element;
    }
}

Now we wanted to use this Container class to hold the boxes and strings lets see how will it happen-

class Box {
    private String id;
    private String name;

    public Box(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Box{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

We created a Box Class now let’s use it with Container class.

   
Container boxContainer = new Container();
boxContainer.setElement(new Box("1", "Box 1"));
Box box = (Box) boxContainer.getElement();
System.out.println(box);

Container stringContainer = new Container();
boxContainer.setElement("Jack");
String string = (String) stringContainer.getElement();
System.out.println(string);

//String box1 = (String) boxContainer.getElement();

Now we can see whenever we are trying to get the element from the container we have to typecast it to the explicit type. There are chances of missing the type or specifying the wrong type while using it as we did in the last line and gets stuck on runtime debugging ClassCastException.

To avoid the above problem, we can create a Container class as a Generic class and ask the user to pass the type parameter while creating an instance of it. Let’s see it in action-

class GenericContainer<T> {
    private T element;

    public T getElement() {
        return element;
    }

    public void setElement(T element) {
        this.element = element;
    }
}

Here we created the above class a GenericClass by specifying the type parameter as T. lets use it and see the difference and advantage of it.

GenericContainer<Box> boxContainer = new GenericContainer<>();
boxContainer.setElement(new Box("1", "Box 1"));
Box box = boxContainer.getElement();

System.out.println(box);

GenericContainer<String> stringContainer = new GenericContainer<>();
stringContainer.setElement("Jack");
String string = stringContainer.getElement();
System.out.println(string);

As we see here, we are not typecasting the value while getting the element from the Container class. It introduces type safety in our code as well as eliminating any possible runtime ClassCastException.

The same is true for Generic interfaces as well. Let’s see it by a quick example-

//Generic interface definition
interface GenericInterface<T1, T2> {
    T2 doOneOperation(T1 t);

    T1 doSecondOperation(T2 t);
}

//A class implementing generic interface
class DemoClass implements GenericInterface<String, Integer> {
    public Integer doOneOperation(String t) {
        //some code
        return -1;
    }

    public String doSecondOperation(Integer t) {
        //some code
        return null;
    }
}

6.2 Java Generic Method or Constructor

In the same way, how we did for classes, interfaces we can generalize methods (known as java generic methods), constructors. We can have only one method declaration and reuse it with different arguments. Java compiler will take care of which type to pass and return from it. There are few properties for the generic methods listed below-

  • Generic methods have a type parameter (the diamond operator enclosing the type) before the return type of the method declaration.
  • We can bound type parameters (we explain bounds later in the article) in generic methods.
  • Generic methods can have different type parameters separated by commas in the method signature.
  • The body of generic methods is like normal methods.

Let’s take an example of converting an array to list of object –

    public static <T> List<T> fromArrayToList(T[] a) {
        return Arrays.stream(a).collect(Collectors.toList());
    }

In the above code snippet, we created a stream from the input array and collected each element of it into a list and finally returning it. Thanks to Java8 Lambda functions. Now let’s see how we can use the same method with different parameter types. Let’s start with an array of strings and integers-

String[] namesArray = {"Jack", "John", "Nick"};
List<String> namesList = fromArrayToList(namesArray);
System.out.println(namesList);

Integer[] numberArray = {1, 2, 3, 4, 5};
List<Integer> numberList = fromArrayToList(numberArray);
System.out.println(numberList);

In the above code snippet, we created an array of names and another array of numbers. We can pass both types of parameters here in the fromArrayToList() method. It handles the value to be returned. It is due to the type parameter before the return type in the method declaration. Now, let’s use the same method to convert an employee object from array to list-

class Employee {
    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

This is the normal employee class now let’s use this class-

Employee[] employeeArr = {new Employee("Jack"), new Employee("John"), new Employee("Nick")};
List<Employee> employeeList = fromArrayToList(employeeArr);

In the same way, how we applied generics on method, we can use it on constructors also. Let’s see it by an example-

class Test {
	//Generics constructor
	public <T> Test(T item){
		 System.out.println("Value of the item: " + item);
		 System.out.println("Type of the item: " 
				 + item.getClass().getName());
	}
}
 
public class GenericsTest {	
	public static void main(String args[]){
		//String type test
		Test test1 = new Test("Test String.");
		Test test2 = new Test(100);
	}
}

In the above code snippet, we created a Test class and its constructor as a typed constructor which can take any object. Next, we used the same constructor with string and integer data-type. Below is the output of the above code-

Value of the item: Test String.
Type of the item: java.lang.String
Value of the item: 100
Type of the item: java.lang.Integer

6.3 Bounded Generics

Until now we have only analyzed Generics used in the type parameter, We said that the type parameter can accept any Object or type. But what If we wanted to restrict the type-parameter to take only specific type of values like the Employee, Number, etc. In these situations, we use Bounded Generic to solve our problem.

By using type-parameter with the bounded scope, we can restrict it to accept some specific values. We can use it in two ways-

The keyword extends is used to mean that the type T extends or implements the upper bound of class or interface. Let’s see an example of how to use it-

    public static <T extends Number> List<T> fromArrayToListForNumbers(T[] a) {
        return Arrays.stream(a).collect(Collectors.toList());
    }

Explanation: Here we created fromArrayToListForIntegers() with type parameter with upper bound as extending from Number class. Now let’s use it with different arrays.

Integer[] intsArray = {1, 2, 3, 4, 5, 6};
Float[] floatsArray = {1.4f, 2.3f, 3.5f, 4.7f, 5.6f, 6.0f};
Double[] doublesArray = {1.4, 2.3, 3.5, 4.7, 5.6, 6.0};
String[] stringArray = {"Jack", "John", "Nick"};

List<Integer> intsList = fromArrayToListForNumbers(numberArray);
List<Float> floatsList = fromArrayToListForNumbers(floatsArray);
List<Double> doublesList = fromArrayToListForNumbers(doublesArray);
//compile time error
List<String> StringsList = fromArrayToListForNumbers(stringArray);

Explanation: In the above code, we are using fromArrayToListForNumbers() by passing Integer, Float, String, and Double. It works well for Integer, Float, and Double but throws a compile-time error for String because the method has upper-bounded type parameter which can only take parameter extending number class and with String, it is not true.

6.4 Multiple Bounds

Type parameter can have multiple upper-bounds also. To specify multiple bounds, we should place an “&” character in between the upper-bounds. There can be only one class and multiple interfaces. Classes should always come first and interfaces afterwards. Let’s understand it by an example-

// with two upper bounds
public static  <T extends Number & Comparable> List<T>  fromArrayToListForNumbersAndComparable(T[] a) {
    return Arrays.stream(a).collect(Collectors.toList());
}

//with three upper bounds one class and other two as interfaces
public static  <T extends Number & Comparable & Serializable> List<T> fromArrayToListForNumbersAndComparableAndSerializable(T[] a) {
    return Arrays.stream(a).collect(Collectors.toList());
}

Explanation: Here in the first example, we created fromArrayToListForNumbersAndComparable() to have two upper bounds Number and Comparable so the argument with which this method deal with will have to be of type number and comparable. In the same way, the second example we created fromArrayToListForNumbersAndComparableAndSerializable() to have three upper bounds Number, Comparable and Serializable so the argument with which this method deal with will have to be of type number comparable and serializable.

7. Using Wildcards with Generics

The question mark (?) represents wildcards in Java generics. It is used to refer to an unknown type. It is introduced to provide a mechanism to cast one collection of class A to another collection of a subclass or superclass of A.

7.1 Assignment Problem in Java Collection

It is a known fact that Object is the super-type of all Java classes, but a collection of Object is not the super-type of any other collection. Let’s take an example to understand it-

For example, a List<Object> is not the super-type of List<Integer>. Assigning a variable of type List<Object> to a variable of type List<Integer> will cause a compiler error. This is to prevent conflicts in collection objects that may happen if we add heterogeneous data-types into it.

The same rule applies to all collection of a type and its subtypes. Consider this example where the Accountant class and Manager class are the subclasses for Employee-

class Employee {
    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class Manager extends Employee {

    public Manager(String name) {
        super(name);
    }
}

class Accountant extends Employee {

    public Accountant(String name) {
        super(name);
    }
}

Now, let’s create two lists-

List<Employee> employeeList = Arrays.asList(new Employee("Jack"), new Employee("John"));
List<Accountant> accountantList = Arrays.asList(new Accountant("Mike"), new Accountant("Smith"));

Can we assign the list of Accountants to List of Employees or vice versa?

employeeList = accountantList;
accountantList = employeeList;

Both the above assignments are not possible because-

In employeeList, we can have an instance of either Employee or its subclasses Accountant or Manager. If we can do accountantList = employeeList, then we can assign Manager instance into Accountant which violates the declaration statement for accountantList that it will hold an instance of Accountant.

Likewise, employeeList =  accountantList; the assignment is not valid because at the end accountantList will point to the same reference which employeeList are pointing to indirectly it has to point to both Manager and Employee which is again a violation of the declaration statement.

Because of the above restriction if we have a method like below-

private static void doSomething(List<Employee> employees) {
    employees.forEach(e -> {
          //do some thing
    });
}

We cannot use the same method for the Accountant and Manager. To solve this problem, we have wildcard character ? in Java Generics. We can use it in three ways-

List<?>                  unknownWildCard = new ArrayList<Employee>();
List<? extends Employee> extendWildCard = new ArrayList<Employee>();
List<? super   Employee> superWildCard = new ArrayList<Employee>();

7.2 Unknown Wildcard

The question mark symbol (?) in List<?> denotes an unknown wildcard. It can accept any lists. For example, List, List, List, etc. When we access an element from the list, its type will be Object. Since we have solved our problem, we can rewrite the method as

private static void doSomething(List<?> employees) {
    employees.forEach(e -> {
          //do some thing
    });
}

But this comes with one more problem. If we use the getName() method, we would have to typecast it first, then use it.

7.3 extends Wildcard Boundary

To solve the above problem, we can define a boundary for our wildcard by saying that it can only hold Employee or its subclass instance. Now we solved our problem, and the modified solution is as below- 

private static void doSomething(List employees) {
    employees.forEach(e -> {
          //do some thing
    });
}

Not only does it solve our problem but it also restricts this method to be used by List of the employee or its subclass objects only. Here we are defining an upper boundary for our type parameter, so it is called upper bound for the generic type parameter. We also call this feature of Generics as the covariance.

7.4 super Wildcard Boundary

The above solution solves our problem while accessing the element from the list and gives a type-safe way. What if we wanted a type-safe we to do the insert operations in our collection objects? This is where we have to restrict our type parameter to accept either its superclass object or its object.

We can do it by specifying the lower boundary for our type parameter by using the super keyword as follows-

public static void insertElements(List list) {
        list.add(new Accountant("Employee"));
        list.add(new Employee("Accountant"));
        list.add(new Manager("Manager"));
    }

We also call this feature of Generics as the contravariance.

8. Bonus Point

Why is it not possible to use generics with primitive data type?

It is very simple to understand why it is not possible to use generics with a primitive data type. It is not possible to use generics with primitive data type because generics are a compile-time feature if Java. There is no existence of generics at runtime. All type parameter ultimately gets converted to Object. So the element which we use with generics must be convertible to Object Type. Since primitive data types don’t extend Object class and can’t be convertible to Object, that is why it is not possible to use primitive data types with Java Generics.

9. Download the Source Code

That was all about Java Generics example. Hope you enjoyed it.

Download
You can download the full source code of this example here: Java Generics Examples

Last updated on Aug. 29th, 2019

Vipul Kumar

Vipul is a Senior Software Engineer with experience in different software technologies including Java, Javascript, Angular, React, Material Design, databases (MySQL), HTML/CSS and even AWS, Big Data and Machine Learning. He likes learning new technologies and using the latest libraries in his work.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button