JDK 5 introduced Java Generics with the aim of reducing bugs and adding an extra layer of abstraction over types.

What You Need

  • About 12 minutes
  • A favorite text editor or IDE
  • Java 8 or later

1. The Need for Generics

The essence of generics is to parameterize types.

That is to say, during the use of generics, the data type of the operation is specified as a parameter.

The significance of introducing generics consists of :

1. It can be used to execute the same code on multiple data types;

Below code snippet is an example without using generics, each type needs to implement an add method.

public class GenericsTest1 {
    public static void main(String[] args) {
        System.out.println(max(1, 2));
        System.out.println(max(1f, 2f));
        System.out.println(max(1d, 2d));
    }

    private static int max(int a, int b) {
        return a > b ? a : b;
    }

    private static float max(float a, float b) {
        return a > b ? a : b;
    }

    private static double max(double a, double b) {
        return a > b ? a : b;
    }
}

The output of above code snippet is below :

2
2.0
2.0

By using generics, we need only one add method.

public class GenericsTest2 {
    public static void main(String[] args) {
        System.out.println(max(1, 2));
        System.out.println(max(1f, 2f));
        System.out.println(max(1d, 2d));
    }

    private static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}

Below is the output of above code snippet :

2
2.0
2.0

2. The compiler will check the type in the generic and no cast is required when using it.

In below code snippet, when we use the list, the elements in the list are all of Object type.

So when taking out the collection elements, we need to artificially force the type to be converted to a specific target type, and java.lang.ClassCastException is easy to occur.

import java.util.ArrayList;
import java.util.List;

public class GenericsTest3 {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add(1);
        list.add("1");
        list.add(1l);
        list.add(1d);
        list.add(1f);

        list.stream().forEach(e -> {
            if (e instanceof Integer) {
                System.out.println("Integer : " + (Integer) e);
            }

            if (e instanceof String) {
                System.out.println("String : " + (String) e);
            }

            if (e instanceof Long) {
                System.out.println("Long : " + (Long) e);
            }

            if (e instanceof Double) {
                System.out.println("Double : " + (Double) e);
            }

            if (e instanceof Float) {
                System.out.println("Float : " + (Float) e);
            }
        });
    }
}

The output of above code snippet is below :

Integer : 1
String : 1
Long : 1
Double : 1.0
Float : 1.0

By using generics, it will provide type constraints and compile-time checks.

import java.util.ArrayList;
import java.util.List;

public class GenericsTest4 {
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();

        intList.add(1);

        intList.forEach(System.out::println);

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

        stringList.add("1");

        stringList.forEach(System.out::println);

        List<Long> longList = new ArrayList<>();

        longList.add(1l);

        longList.forEach(System.out::println);

        List<Float> floatList = new ArrayList<>();

        floatList.add(1f);

        floatList.forEach(System.out::println);

        List<Double> doubleList = new ArrayList<>();

        doubleList.add(1d);

        doubleList.forEach(System.out::println);
    }
}

Below is the output of above code snippet :

Integer : 1
String : 1
Long : 1
Double : 1.0
Float : 1.0

2. Basic usage of generics

When using generics, the parameter type can be used in classes, interfaces and methods, and are called generic classes, generic interfaces and generic methods respectively.

2.1 Generic Classes

A Generic class simply means that the items or functions in that class can be generalized with a parameter, for example T.

It specifies that we can add any type as a parameter in place of T like Integer, Character, String, Double or any other user-defined type.

public class GenericsTest5 {
    public static void main(String[] args) {
        Pair<String, String> name = new Pair<>("name", "tom");

        System.out.println(name);

        Pair<String, Integer> age = new Pair<>("age", 18);

        System.out.println(age);
    }

    private static class Pair<K, V> {
        private K key;
        private V value;

        public Pair(K key, V value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Pair [key=" + key + ", value=" + value + "]";
        }
    }
}

The output of above code snippet is below :

Pair [key=name, value=tom]
Pair [key=age, value=18]

2.2 Generic Interfaces

A generic interface deals with different data types and allows putting constraints on those data types.

public class GenericsTest6 {
    public static void main(String[] args) {
        MaxMin<Integer> c1 = new MaxMin<>();

        System.out.println("int min : " + c1.min(1, 2));
        System.out.println("int max : " + c1.max(1, 2));

        MaxMin<Double> c2 = new MaxMin<>();

        System.out.println("double min : " + c2.min(1d, 2d));
        System.out.println("double max : " + c2.max(1d, 2d));
    }

    private static interface MyComparable<T extends Comparable<T>> {
        T max(T a, T b);

        T min(T a, T b);
    }

    private static class MaxMin<T extends Comparable<T>> implements MyComparable<T> {
        @Override
        public T max(T a, T b) {
            return a.compareTo(b) < 0 ? b : a;
        }

        @Override
        public T min(T a, T b) {
            return a.compareTo(b) < 0 ? a : b;
        }
    }
}

Below is the output of above code snippet :

int min : 1
int max : 2
double min : 1.0
double max : 2.0

2.3 Generic Methods

A generic method is a method that we can call with arguments of different data types.

It must have a type parameter (the diamond operator enclosing the type) before the return type of the method declaration in order to declare that this is a generic method.

It is also possible and sometimes necessary to explicitly indicate the data type, especially if the compiler cannot infer it.

public class GenericsTest7 {
    public static void main(String[] args) {
        System.out.println("int min = " + min(1, 2));
        System.out.println("int max = " + max(1, 2));

        System.out.println("double min = " + min(1d, 2d));
        System.out.println("double max = " + max(1d, 2d));

        System.out.println("float min = " + GenericsTest7.<Float>min(1f, 2f));
        System.out.println("float max = " + GenericsTest7.<Float>max(1f, 2f));
    }

    private static <T extends Comparable<T>> T min(T a, T b) {
        return a.compareTo(b) > 0 ? b : a;
    }

    private static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}

The output of above code snippet is below :

int min = 1
int max = 2
double min = 1.0
double max = 2.0
float min = 1.0
float max = 2.0

Without explicitly indicating the data type, if there are several types in the method, the data type is the lowest level of the same parent classes, up to Object.

In the case of indicating the data type, the data type can also be its subclass.

 1import java.util.Random;
 2
 3public class GenericsTest22 {
 4    public static void main(String[] args) {
 5        int c1 = choose(1, 2);
 6
 7        System.out.println(c1);
 8
 9        Number c2 = choose(1, 2.0);
10
11        System.out.println(c2);
12
13        Object c3 = choose(1, "2");
14
15        System.out.println(c3);
16
17        int c4 = GenericsTest22.<Integer>choose(1, 2);
18
19        System.out.println(c4);
20
21        // GenericsTest22.<Integer>choose(1, 2.0);
22
23        Number c5 = GenericsTest22.<Number>choose(1, 2.0);
24
25        System.out.println(c5);
26
27        Object c6 = GenericsTest22.<Object>choose(1, "2");
28
29        System.out.println(c6);
30    }
31
32    private static <T> T choose(T a, T b) {
33        Random r = new Random();
34        int low = 1;
35        int high = 100;
36        int result = r.nextInt(high - low) + low;
37
38        if (result <= 50) {
39            return a;
40        }
41
42        return b;
43    }
44}

Below is the output of above code snippet :

2
2.0
2
2
1
1

At line 21, if we put this line of code out of comment, it will display below error :

The parameterized method <Integer>choose(Integer, Integer) of type GenericsTest22 is not applicable for the arguments (Integer, Double)

2.4 Generic Constructors

A constructor is a block of code that initializes the newly created object.

It is an instance method with no return type.

The name of the constructor is same as the class name.

A generic constructor is a constructor the same as a generic method.

For a generic constructor, the type parameter must be placed before the class name.

Constructors can be Generic, despite its class is not Generic.

public class GenericsTest9 {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;

        compare(a, b, new Comparison(a, b));

        double c = 1d;
        double d = 2d;

        compare(c, d, new Comparison(c, d));
    }

    private static <T extends Comparable<T>> void compare(T a, T b, Comparison c) {
        if (c.bigger) {
            System.out.println(a + " is bigger than " + b);
        } else {
            System.out.println(a + " is smaller than " + b);
        }
    }

    private static class Comparison {
        boolean bigger;

        <T extends Comparable<T>> Comparison(T a, T b) {
            if (a.compareTo(b) > 0) {
                this.bigger = true;
            }
        }
    }
}

The output of above code snippet is below :

1 is smaller than 2
1.0 is smaller than 2.0

In below code snippet, the constructors is not generic even if it is in a generic class.

public class GenericsTest8 {
    public static void main(String[] args) {
        Comparison<Integer> c1 = new Comparison<Integer>(1, 2);

        System.out.println(c1.max());

        System.out.println(c1.min());

        Comparison<Double> c2 = new Comparison<Double>(1d, 2d);

        System.out.println(c2.max());

        System.out.println(c2.min());
    }

    private static class Comparison<T extends Comparable<T>> {
        T a;
        T b;

        Comparison(T a, T b) {
            this.a = a;
            this.b = b;
        }

        T max() {
            return this.a.compareTo(this.b) > 0 ? this.a : this.b;
        }

        T min() {
            return this.a.compareTo(this.b) > 0 ? this.b : this.a;
        }
    }
}

Below is the output of above code snippet :

2
1
2.0
1.0

3. Wildcards and Boundary

Let us have a look at below code snippet :

 1public class GenericsTest10 {
 2    public static void main(String[] args) {
 3        Dog dog = new Dog();
 4
 5        Animal animal = dog;
 6
 7        System.out.println(dog == animal);
 8    }
 9
10    private static class Animal {
11
12    }
13
14    private static class Dog extends Animal {
15
16    }
17}

The output of above code snippet is below :

true

Everything is OK, there is no error at line 5 when we try to assign a dog to an animal.

But when it comes to a list of dogs to be assigned to a list of animals, it will not be ok, we will have type mismatch compilation error.

 1import java.util.ArrayList;
 2import java.util.List;
 3
 4public class GenericsTest11 {
 5    public static void main(String[] args) {
 6        List<Dog> dogs = new ArrayList<>();
 7
 8        List<Animal> animals = dogs;
 9
10        System.out.println(animals == dogs);
11    }
12
13    private static class Animal {
14
15    }
16
17    private static class Dog extends Animal {
18
19    }
20}

In above code snippet, at line 8, we will have below error :

Type mismatch: cannot convert from List<GenericsTest11.Dog> to List<GenericsTest11.Animal>

To make it work, we can use a wildcard which represents a list of unknown type.

import java.util.ArrayList;
import java.util.List;

public class GenericsTest13 {
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();

        // Type mismatch : cannot convert from List<Dog> to List<Animal>
        // List<Animal> animals = dogs;

        List<?> animals = dogs;

        System.out.println(animals == dogs);
    }

    private static class Animal {

    }

    private static class Dog extends Animal {

    }
}

Or use a wildcard with an upper boundary which allows to specify a super type that the type argument must inherit.

import java.util.ArrayList;
import java.util.List;

public class GenericsTest11 {
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();

        // Type mismatch : cannot convert from List<Dog> to List<Animal>
        // List<Animal> animals = dogs;

        List<? extends Animal> animals = dogs;

        System.out.println(animals == dogs);
    }

    private static class Animal {

    }

    private static class Dog extends Animal {

    }
}

Or use a wildcard with a lower boundary which allows to specify a class whose type argument must be a super class.

import java.util.ArrayList;
import java.util.List;

public class GenericsTest12 {
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();

        // Type mismatch : cannot convert from List<Dog> to List<Animal>
        // List<Animal> animals = dogs;

        List<? super Dog> animals = dogs;

        System.out.println(animals == dogs);
    }

    private static class Animal {

    }

    private static class Dog extends Animal {

    }
}

The output of above 3 code snippet is below :

true

Pay attention that List<?> is not List<Object>.

In fact, List<?> is equivalent to List<? extends Object>.

If use List<Object>, there is still type mismatch error.

 1import java.util.ArrayList;
 2import java.util.List;
 3
 4public class GenericsTest11 {
 5    public static void main(String[] args) {
 6        List<Dog> dogs = new ArrayList<>();
 7
 8        List<Object> animals = dogs;
 9
10        System.out.println(animals == dogs);
11    }
12
13    private static class Animal {
14
15    }
16
17    private static class Dog extends Animal {
18
19    }
20}

In above code snippet, at line 8, even if List<Object> is used, we will still have below type mismatch error :

Type mismatch: cannot convert from List<GenericsTest11.Dog> to List<Object>

The parameterized type can have several boundaries using the intersection of types &.

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class GenericsTest14 {
    public static void main(String[] args) {
        List<Person> ps = new ArrayList<>();

        ps.add(new Person(18, "tom"));
        ps.add(new Person(16, "jerry"));

        System.out.println(max(ps));

        List<Student> ss = new ArrayList<>();

        ss.add(new Student(18, "tom", 1));
        ss.add(new Student(16, "jerry", 2));

        System.out.println(max(ss));
    }

    private static class Person implements Comparable<Person>, Serializable, Cloneable {
        Integer age;
        String name;

        public Person(Integer age, String name) {
            this.age = age;
            this.name = name;
        }

        @Override
        public int compareTo(Person o) {
            return this.age.compareTo(o.age);
        }

        @Override
        public String toString() {
            return "Person [age = " + age + ", name = " + name + "]";
        }
    }

    private static class Student extends Person {
        Integer id;

        public Student(Integer age, String name, Integer id) {
            super(age, name);
            this.id = id;
        }

        @Override
        public int compareTo(Person o) {
            if (o instanceof Student) {
                return this.id.compareTo(((Student) o).id);
            }
            return super.compareTo(o);
        }

        @Override
        public String toString() {
            return "Student [id = " + id + ", age = " + age + ", name = " + name + "]";
        }
    }

    private static <T extends Comparable<? super T> & Serializable & Cloneable> T max(List<T> elements) {
        T max = elements.get(0);

        for (int i = 0; i < elements.size(); i++) {
            T current = elements.get(i);

            if (current.compareTo(max) > 0) {
                max = current;
            }
        }

        return max;
    }
}

Below is the output of above code snippet :

Person [age = 18, name = tom]
Student [id = 2, age = 16, name = jerry]