Free Java Tutorials >> Table of contents >> Generics

1. Generics

Generic Classes

Generics allow us to have flexible variable types within a class. Imagine you creating the following classes to contain a pair of objects:

class PairInt {
	int a, b;
}
class PairFloat {
	float a, b;
}
class PairString {
	String a, b;
}

The above classes are identical besides the type of the internal fields. Thus, we can make the variable type generic (a "parameter"):

//This is read "class Pair generic on T":
class Pair<T> {
	T a, b;
}

The <T> is the templated type above. Note that generics may sometimes also be called "type parameters" or "templates" in some languages.

Once this Pair class is defined, it can be used as so:

class Pair<T> {
	T a, b;
}
public class MyProgram {
    public static void main(String[] args) {
		//Using the pair class
        Pair<Integer> x = new Pair<Integer>();
        x.a = 1;
        x.b = 41;
        
        Pair<String> y = new Pair<String>();
        y.a = "First";
        y.b = "Second";
        
        System.out.println(x.a+x.b);
        System.out.println(y.a+y.b);
    }
}

Notice that a variable type must be specified when instantiated Pairs. In addition, Java generics can only use object types, not primitives. This is a limitation due to how generics are implemented in Java.

Generics and Methods

Methods and constructors can use the type parameter as arguments or return types:

class Pair<T> {
	T first, second;
	public Pair(T first, T second) {
		this.first = first;
		this.second = second;
	}
	public T getFirst() { return first; }
	public T getSecond() { return second; }
}

Generic Methods

Method declarations can specify their own parameter. This should be done before the return type of the method, such as the T here:

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); 
    }
}

Wildcards

There are cases where you are using a generic class and do not care about the parameterized type. In these cases, you may use a question mark as the templated type:

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

Thus, the method above will take a Collection with any parameter

2. Generics and Polymorphism

This advanced section relies on polymorphism topics taught later in the textbook.

Bounded parameters

You can force a templated class to be a subclass of a specified type using the "extends" keyword within the parametrization:

public class NaturalNumber<T extends Integer> {
    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }
}

In the above example, the isEven method is allowed to perform n.intValue() because the Integer class has an intValue method.

Multiple bounds: Bounded types can force inheritance from multiple types using the following syntax. Note that since Java only supports multiple inheritance through interfaces, at most one of the types can be a class or abstract class:

<T extends B1 & B2 & B3>

Upper bounded wildcards

To restrict a wildcard class into being a subtype of another type, use the ? extends keywords:

public static void process(List<? extends Foo> list) {
    for (Foo elem : list) {
        // ...
    }
}

Lower bounded wildcards

You can write a function that will accept a generic type or any superclass of it. Often, this is useful when the function uses a specific class in conjunction with a collection, and the collection needs only to accept the specific class:

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

In the above example, addNumbers will add the numbers 1 to 10 to either a List generic on Integer, Number, or Object

Type erasure and overloading

Generics are a compile time feature in Java, and thus the compiled code no longer knows about the types. For example, the following code is the same after compile time:

List<Integer> x = new List<Integer>();
x.add(21);
System.out.println(x.get(0)*2); //42

//This is the same after compilation:
List y = new List();
y.add(21);
System.out.println(  (Integer)(y.get(0))   * 2);

The way that generics are lost after compile time is called type erasure. One consequence is that overloaded methods must differ after type erasure. For example, the following two methods are identical:

void example(List<String> x) {}
void example(List<Integer> y) {}

Thus if you tried to write a class with both of the above methods, it would not be able to compile.

Limitations of generics

No primitive generics: Since generics are lost at compile time, all generics use the "Object" type at run time. Thus, every generic must be an object type. You cannot be generic on an int, byte, short, float, boolean, etc.

Cannot instantiate generics: Since generics do not specify whether the type has a default constructor, you cannot instantiate generics. If you have a generic type T, then you may not do new T().

No arrays of generics: You cannot create arrays of generic types, due to some edge cases involve type erasure. Thus, new List<Integer>[2] is not allowed.

Previous: References

Next: Collections