Skip to content

Generic types

Introduction

Generics are also known as templates. They allow for class/method/interface parameterization, which is carried out by passing the types of arguments when they are actually used in the code. Using such a mechanism, we can reuse the same code fragments because they are not rigidly related to any particular implementation. Thanks to generics, we are also able to get rid of unnecessary projection.

Consider the class example:

public class Box {

    private Object item;

    public Object getItem() {
        return item;
    }

    public void setItem(Object item) {
        this.item = item;
    }
}

In the case of the above code fragment, we are able to pass any object to an object of the Box class (due to the use of the Object type). However, at the compilation level, we are not protected in any way from mistakenly passing different types of objects to the same Box object, which can lead to errors in the runtime. Due to the possibility of using generic types, the compiler is able to validate the transferred types.

We define the name of the generic type in brackets <, >, right after the class name. We can use this name throughout the class, e.g .:

public class Box<T> {
    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

When comparing the above code snippets, it can be seen that the type T replaces the type Object. The specific type for the T parameter is passed when creating an instance of the class or using static methods of the class.

Naming convention

By convention, generics are most often written with a single uppercase letter. It is suggested to use the following nomenclature forms:

  • E - Element (used e.g. for Java Collection API)
  • K - Key
  • N - Number
  • T - Typ
  • V - Value

Creating instances of generic classes

In order to create an object to which we need to pass a value of a generic type, we must provide a specific type that replaces the parameter, e.g. T:

new Box<Integer>();

An object created in this way can be assigned to an appropriate reference, which should also have information about the generic type specified, i.e .:

Box<Integer> numberBox = new Box<Integer>(); // T replaced with Integer

In Java, if the reference to which we assign the created generic object contains information about the type, we do not have to repeat this type when creating the object, but the use of <> is still mandatory:

Box<Integer> numberBox = new Box<>(); // <> required

In turn, this type must be specified when creating an object, if we use the var keyword, e.g .:

var intList = new ArrayList<Integer>();

Number of generics

Within a single class, you can declare multiple generics that will be part of the class. Each type parameter should be declared as a unique character by convention.

The following example defines a pair of two generic objects. Both the key field and thevalue field can be of any type.

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

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }
}
Creating an instance for the class above will look like this:
Pair<String, Float> pair = new Pair<String, Float>();

Extending a generic class

We can easily extend the generic class. A class that inherits a generic class must specify a generic type or remain generic. The following class definitions demonstrate these possibilities:

public class BaseClass<T, V> {
}

public class NoLongerGenericClass extends BaseClass<String, Integer> { // generics are specified
}

public class StillGenericClass<T> extends BaseClass<T, Integer> { // one generic type was specified - V. The class is still generic and requires that the parameter T be specified.
}

Generic methods

Not only classes can be generic. Methods also allow you to declare your own parametric types. The visibility of these types is limited to the specific method (signature and body). Both static and non-static generic methods are allowed. The syntax for generic methods extends the method declaration to include parametric types, placed before the return type. When using such methods, we don't have to, but we can specify generics. We do it with the use of parentheses <>, in which we give the values of generics, e.g.:

public class PairGenerator {
  public static <K, V> Pair<K, V> generatePair(K key, V value) {
    Pair<K, V> pair = new Pair<K, V>();
    pair.setKey(key);
    pair.setValue(value);
    return pair;
  }

  public static void main(String[] args) {
    final Pair<Integer, String> firstPair = PairGenerator.generatePair(1, "value1");
    final Pair<Long, String> secondPair = PairGenerator.<Long, String>generatePair(2L, "value2");
  }
}

Limiting parameter types

Typically, we want the generic classes we create to be able to use only certain values of generics, such as those that inherit from a certain class or implement a specific interface. For this purpose, we use the so-called * bounded types parameters *. The declaration is made by specifying a parametric type, then using the extends keyword and defining a constraint.

public class NumberBox<T extends Number> {

  private T value;

  public T getValue() {
    return value;
  }

  public void setValue(T value) {
    this.value = value;
  }

  public static void main(String[] args) {
    NumberBox<Double> doubleBox = new NumberBox<>();
    doubleBox.setValue(3.3);
    NumberBox<Integer> intBox = new NumberBox<>();
    intBox.setValue(10);
    System.out.println(intBox.getValue() + " " + doubleBox.getValue());
  }
}

For bounded types parameters we use the extends keyword for both classes and interfaces. Moreover, it is possible to force the generic type to be used to implement many interfaces, e.g .:

public class NumberBox<T extends Number & Cloneable & Comparable<T>>

Subtypes

For generics, it is a mistake to treat type parameters the same way as generic classes, e.g. if Integer is a subtype ofNumber, it does not mean that Box <Integer> is a subtype of Box <Number >.

Java Platform Generics subtypes

In order to obtain the expected relationship, you should use the so-called Wildcards.

Wildcards

For generics, the '?' Character represents the unknown type. Wildcard can represent:

  • variable type
  • class field type
  • optional return type.

It cannot, however, represent:

  • the argument of the generic method
  • the generic class argument.

Upper limit

Suppose we want to create a method that will work for lists containing any numeric type (ie objects that inherit from the Number class). For this purpose, we can use the so-called upper-bounded wildcard, which is represented by the character?and the keywordextends, e.g .:

public class UpperBoundedWildcards{

  public static double sum(final List<? extends Number> numbers) { // the method accepts only types extending the Number class
    double sum = 0;
    for (Number number : numbers) {
      sum += number.doubleValue();
    }
    return sum;
  }

  public static void main(String[] args) {
    List<Integer> values = List.of(1, 2, 3);
    System.out.println(sum(values));
  }
}

Lower bound

Suppose we need to write a method that takes as an argument a list of objects of type Integer or from which this class inherits (e.g.Number or Object). For this purpose, we can use the so-called A lower-bounded wildcard, which is represented by the ?character and the keyword super, which matches the specified class and all parent classes.

public class LowerBoundedWildcards {

  public static void main(String[] args) {
    addNumbers(List.of(1, 2, 3));
    addNumbers(List.of(new Object(), new Object(), new Object()));
  }

  public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
      list.add(i);
    }
  }
}