Java Generics Concepts

JavaJavaBeginner
Practice Now

Introduction

Generics in Java allow writing flexible and reusable code by providing an additional layer of abstraction. They allow creating a single algorithm for multiple types of objects. Generics provide type safety and avoid runtime errors. In this lab, we will understand various aspects of Generics in Java.

Creating a Generic Class

We can create a Generic class in Java with the help of the diamond operator (<>). In the following code block, we will create a Generic class named MyGenericClass that can accept any type of object. We will also create an object of type MyGenericClass with integer and string types as parameters.

// ~/project/MyGenericClass.java

class MyGenericClass<T> {
    T field1;

    public MyGenericClass(T field1) {
        this.field1 = field1;
    }

    public T getField1() {
        return field1;
    }
}

public class Main {
    public static void main(String[] args) {
        MyGenericClass<Integer> myIntObj = new MyGenericClass<Integer>(100);
        MyGenericClass<String> myStringObj = new MyGenericClass<String>("Hello World");

        System.out.println(myIntObj.getField1());
        System.out.println(myStringObj.getField1());
    }
}

To run the code, open the terminal and navigate to the ~/project directory. Compile the code with the following command:

javac MyGenericClass.java

After successful compilation, run the code with the following command:

java Main

The output of the code will be:

100
Hello World

Creating a Generic Method

We can also create a Generic method in Java. The Generic method can work with different types of objects. In the following code block, we will create a Generic method named printArray that can print any type of array.

// ~/project/Main.java

public class Main {
    public static <T> void printArray(T[] array) {
        for (T element : array)
            System.out.println(element);
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        Double[] doubleArray = {1.1, 2.2, 3.3, 4.4};
        String[] stringArray = {"Hello", "World"};

        printArray(intArray);
        printArray(doubleArray);
        printArray(stringArray);
    }
}

To run the code, open the terminal and navigate to the ~/project directory. Compile the code with the following command:

javac Main.java

After successful compilation, run the code with the following command:

java Main

The output of the code will be:

1
2
3
4
5
1.1
2.2
3.3
4.4
Hello
World

Creating a Bounded Generic Method

We can also create a Bounded Generic method. Bounded Generics limit the range of types accepted by the method. We use the extends keyword to impose the bound. In the following code block, we will create a Bounded Generic method named printNumbers that can print the numbers of objects of type Number and its subclasses.

// ~/project/Main.java

public class Main {
    public static <T extends Number> void printNumbers(T[] numbers) {
        for (T number : numbers)
            System.out.println(number);
    }

    public static void main(String[] args) {
        Integer[] intNumbers = {1, 2, 3, 4, 5};
        Double[] doubleNumbers = {1.1, 2.2, 3.3, 4.4};

        printNumbers(intNumbers);
        printNumbers(doubleNumbers);
    }
}

To run the code, open the terminal and navigate to the ~/project directory. Compile the code with the following command:

javac Main.java

After successful compilation, run the code with the following command:

java Main

The output of the code will be:

1
2
3
4
5
1.1
2.2
3.3
4.4

Understanding Type Safety in Generics

Generics provide type safety, ensuring that we define only one type of object to work with at a time. In the following code block, we will create an ArrayList of integers without defining its type parameter. It is a bad practice to use Generics without type parameters. The code will compile, but it will return a runtime error when we try to access its elements of other types.

// ~/project/Main.java

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList integerList = new ArrayList();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);
        integerList.add("Hello World");

        for (Object element : integerList) {
            System.out.println((Integer)element);
        }
    }
}

To run the code, open the terminal and navigate to the ~/project directory. Compile the code with the following command:

javac Main.java

After successful compilation, run the code with the following command:

java Main

The output of the code will be:

1
2
3
Exception in thread "main" java.lang.ClassCastException: java.base/java.lang.String cannot be cast to java.base/java.lang.Integer
    at Main.main(Main.java:13)

We get a runtime error because the integerList contains elements of the Integer type and String type. This is because we did not define the type parameter while creating the integerList.

Understanding Type Erasure in Generics

Java uses Type Erasure in Generics to ensure that no additional overhead is needed at runtime. The compiler replaces the type parameter with the Object class or the parent class (in case of bounded Generics) during the process of type erasure. In the following code block, we will see how Generics works with primitive types at compile time and runtime.

// ~/project/Main.java

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> integerList = new ArrayList<Integer>();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);

        int sum = 0;
        for (int i = 0; i < integerList.size(); i++) {
            sum += integerList.get(i);
        }
        System.out.println(sum);
    }
}

To run the code, open the terminal and navigate to the ~/project directory. Compile the code with the following command:

javac Main.java

After successful compilation, run the code with the following command:

java Main

The output of the code will be:

6

In the above code block, we tried to add the elements of the integerList. As we know, primitive types like int cannot be used with Generics. Due to type erasure, the type parameter Integer is replaced with the Object class at compile time. However, autoboxing and unboxing allowed us to perform the addition of integers at compile time. At runtime, the code performed like normal code execution, without any additional overhead.

Summary

In this lab, we learned about Java Generics. We covered the basic concepts related to Generic classes, Generic methods, Bounded Generic methods, Type Safety in Generics, and Type Erasure in Generics. We also saw how to write flexible and reusable code using Generics. We hope this lab helped you understand Generics in Java.

Other Java Tutorials you may like