Java Tutorials Made Easy banner

  

Section 1 - The Need for Generics

 

Suppose you write a simple class which models a box described by its length, width, and height.

public class Box 
{
    private int length, width, height;

    public Box(int l, int w, int h) 
    { 
        length = l; width = w; height = h; 
    }

    public @Override String toString() 
    { 
        return "l=" + length +" w=" + width 
             + " h=" + height; 
    }
}

Since you like two of everything, you want a container for both boxes which can return either one. This is easy to do by simply writing a TwoBoxes class.

public class TwoBoxes
{
    private Box box1;
    private Box box2;

    public TwoBoxes(Box b1, Box b2) { box1 = b1; box2 = b2; }

    public Box getBox1() { return box1; }
    public Box getBox2() { return box2; }
}

You can easily create a TwoBoxes object which can be used to display both boxes.

        TwoBoxes   tb = new TwoBoxes(new Box(1, 2, 3),
                                     new Box(4, 5, 6));

        System.out.println(tb.getBox1());
        System.out.println(tb.getBox2());

OUTPUT:

l=1 w=2 h=3

l=4 w=5 h=6

 

Life is good, until you realize you also want your container to handle two strings as well. If you try to pass strings to the TwoBoxes constructor, a compilation error occurs.

       TwoBoxes   ts = new TwoBoxes("Hello",  // ERROR: String cannot 
                                    "World"); // be converted to Box

You work around this by writing a TwoStrings class.

public class TwoStrings
{
    private String string1;
    private String string2;

    public TwoStrings(String s1, String s2) 
    { 
        string1 = s1; string2 = s2; 
    }

    public String getString1() { return string1; }
    public String getString2() { return string2; }    
}

You can create a TwoStrings object which can be used to display both strings.

        TwoStrings ts = new TwoStrings("Hello",
                                       "World");

        System.out.println(ts.getString1());
        System.out.println(ts.getString2());

OUTPUT:

Hello

World

 

The problem is that this approach is not scalable. You would have to write a new container class for each type of object that can be contained. What if you could design your container class to hold two of any type? This is accomplished by making your container class generic.

class Two<T>
{
    private T t1;
    private T t2;

    public Two(T arg1, T arg2) { t1 = arg1; t2 = arg2; }

    public T getT1() { return t1; }
    public T getT2() { return t2; }
}

The generic class Two is specified with a type parameter T inside angled brackets. You can create two boxes by specifying Box for the type parameter T. You can also create two strings by specifying String for the type parameter T 

        Two<Box>   tb = new Two<>(new Box(1, 2, 3),
                                  new Box(4, 5, 6));

        System.out.println(tb.getT1());
        System.out.println(tb.getT2());

        Two<String> ts = new Two<>("Hello",
                                   "World");

        System.out.println(ts.getT1());
        System.out.println(ts.getT2());

OUTPUT:

l=1 w=2 h=3

l=4 w=5 h=6

Hello

World

 

Section 2 - Generics: A Closer Look

When defining a generic class, a type parameter should be listed inside angled brackets in the class header after the name of the class. The letter T (for type) is frequently used. The letter (for element) is often used as well.

class MyClass <T> // Generic class with type parameter T

Wherever is encountered inside MyClass, the class which is passed as the type parameter is substituted.  For example, if Box is the type parameter, then field t is of type Box, the constructor accepts a parameter of type Box, and the accessor returns a Box.

     private T t;

     public MyClass(T arg) { t = arg; }

     public T getT() { return t; }

To create an instance of MyClass, the declared type must specify in angled brackets the class to be passed as the type parameter. For example, MyClass<Box> specifies a MyClass of Box, and MyClass<String> specifies a MyClass of String. The angled brackets after the new operator should be left empty and the compiler will infer its type from the declared type. The empty set of brackets is referred to as the diamond operator.  

MyClass<Box>    myBox    = new MyClass<>(new Box(1, 2, 3));

MyClass<String> myString = new MyClass<>("Hello");

A type parameter must be a class and not a primitive type. The following example attempts to create an instance of a generic class using an int primitive parameter, which results in a compilation error.

MyClass<int>    myInt; // ERROR: type parameter must be a class

Section 3 - Raw Types

 

It is possible to create a non-generic instance of a generic class. The following example creates two boxes, but is declared as reference of non-generic type Two.

Two to = new Two(new Box(1, 2, 3),
                 new Box(4, 5, 6));

Non-generic instances substitute the Object class for type parameter T. Therefore, the following statement does not compile even though the objects referenced by field t1 is really a Box.

Box b = to.getT1(); // Error: Object cannot be converted to Box

Casting is necessary to convert the objects to their target type, and erroneous casts will not generate compilation errors. In the following example, casting the Box to a String generates a ClassCastException at runtime.

        Box b = (Box)to.getT1();

        System.out.println(b);
 
        try {
           String s = (String)to.getT2();
        } catch (ClassCastException e) {
            System.out.println(e.getMessage());
        }

OUTPUT:

l=1 w=2 h=3

Box cannot be cast to java.lang.String

 

Section 4 - Generic Methods

 

Suppose you wanted a method that printed both objects in container Two for any type parameter. One approach would be to use type wildcarding. This is done by placing a question mark (?) between the angled brackets of the method’s parameter.

    public static void printTwo(Two<?> t) 
    {
        System.out.println(t.getT1());
        System.out.println(t.getT2());
    }

This method can be called with a Two whose type parameter is any class.

        Two<Box>   tb = new Two<>(new Box(1, 2, 3),
                                  new Box(4, 5, 6));

        Two<String> ts = new Two<>("Hello",
                                   "World");

        printTwo(tb);
        printTwo(ts);

OUTPUT:

l=1 w=2 h=3

l=4 w=5 h=6

Hello

World

 

Another approach is to place a type parameter in angled brackets in the method header after the keyword static but before the method’s return type. As with a generic class definition, all occurrences of the type parameter are substituted by the actual type.

    public static <T> void printTwo(Two<T> t) 
    {
        System.out.println(t.getT1());
        System.out.println(t.getT2());
    }

Section 5 - The Comparable Interface

 

Generics are used throughout the Java API. An example is the Comparable interface. It is generic for type parameter T and defines the compareTo method which compares this object with another object of type T and returns a positive integer if this object is greater, a negative integer if this object is less than, and zero of this object is the same.

public interface Comparable<T>
{
    public int compareTo(T t);
}

To compare two boxes, a Box object must be passed to the compareTo method by the caller. Recall that a generic class substitutes the class passed as the type argument for the type parameter. Therefore, the Box class implements Comparable<Box>. The compareTo method can be implemented by differencing the volumes of the two boxes. 

public class Box implements Comparable<Box> 
{
    ...

    public @Override int compareTo(Box b) 
    {
       return length * width * height 
          - b.length * b.width * b.height; 
    }
}

Section 6 - Type Parameter Restrictions 

 

Suppose you want to write a generic method which selects the larger of two objects. Type parameter can be declared in the method header, then two parameters of type can be passed as arguments to the method. The following example returns the first argument.

    public static <T> T max(T t1, T t2)
    {
        return t1;
    }

As we attempt to improve the method by logically comparing the object, a compilation error occurs, since objects cannot be the operands of operator >.

    public static <T> T max(T t1, T t2)
    {
        if (t1 > t2) // ERROR: Bad operands for binary operator >
            return t1;

        return t2;
    }

Not a problem, since we can use the compareTo method of the Comparable interface to compare two objects. However, since not all classes implement the Comparable interface, a compilation error is generated for the method which is generic for all types T.

    public static <T> T max(T t1, T t2)
    {
        if (t1.compareTo(t2) > 0) // ERROR: Cannot find method 
                                  //        compareTo(T)
            return t1;

        return t2;
    }

If type is restricted so that the max method can only be called for classes that implement the Comparable interface, the method compiles successfully. This is called a type restriction and is implemented by placing the keyword extends after the type parameter, followed by the superclass, followed by the closing angled bracket.

    public static <T extends Comparable<T> > T max(T t1, T t2)
    {
        if (t1.compareTo(t2) > 0)
            return t1;

        return t2;
    }

Since the Box class implements the Comparable interface, the max method can be used to select the larger of two boxes.

        Box b1 = new Box(1, 2, 3);
        Box b2 = new Box(4, 5, 6);

        System.out.println(max(b1, b2));

OUTPUT:

l=4 w=5 h=6

 

Note that the extends keyword is used even though the parent is an interface.

 

Suppose class does not implement the Comparable interface.

class A() {}

Then, a compilation error occurs if objects of class are passed to the max method.

   System.out.println(
       max(new A(), new A())); // ERROR: A is not a subclass of
                               //        Comparable<A> 

Section 7 - Generic Classes with Type Parameter Restrictions

 

The type parameter of a generic class can also be restricted. For example, the type parameter of generic class Two can be restricted to only support types which implement the Comparable interface. This is accomplished by specifying <T extends Comparable<T> > as the type parameter. Since fields t1 and t2 define compareTo, methods larger and smaller can be added to class Two to select the field with the larger or the smaller value.

class Two<T extends Comparable<T> >
{
    private T t1;
    private T t2;

    public Two(T arg1, T arg2) { t1 = arg1; t2 = arg2; }

    public T getT1() { return t1; }
    public T getT2() { return t2; }

    public T larger() 
    { 
        if (t1.compareTo(t2) > 0)
            return t1;
        return t2;
    }

    public T smaller() 
    { 
        if (t1.compareTo(t2) < 0)
            return t1;
        return t2;
    }
}

Since both the Box and String classes implement the Comparable interface, they can be placed in container Two. The following example selects the larger of two boxes and the smaller of two strings.

        Two<Box>   tb = new Two<>(new Box(1, 2, 3),
                                  new Box(4, 5, 6));

        System.out.println(tb.larger());

        Two<String> ts = new Two<>("Hello",
                                   "World");

        System.out.println(ts.smaller());

OUTPUT:

l=4 w=5 h=6

Hello

 

Section 8 - Wildcarding with Type Parameter Restrictions

 

A generic object passed to a method can have its types restricted through use of a wildcard.

Suppose you wrote a Shape class hierarchy that uses the area of each shape for comparison.

public class Shape
{
    public double area() { return 0; }
    @Override public String toString() { return "Shape"; }

    @Override public boolean equals(Object o) 
    { 
        return area() == ((Shape)o).area(); 
    }
}

public class Rectangle extends Shape
{
    protected double length;
    protected double width;

    public Rectangle(double l, double w) { length = l; width = w; }

    @Override public double area() { return length * width; }
    public @Override String toString() 
    { 
        return "l=" + length +" w=" + width; 
    } 
}

public class Square extends Rectangle
{
    public Square(double side) { super(side, side); }
    public @Override String toString() { return "s=" + length; } 
}

Now suppose you want to write a method which returns the larger of the objects in a Two container. If the type is restricted to subclasses of Shape, then the area method can be used to select the larger of the two objects. The type wildcard is restricted using <? extends Shape>.

    public static Shape largerOfTwo(Two<? extends Shape> two) 
    {
        if (two.getT1().area() > two.getT2().area())
            return two.getT1();

        return two.getT2();
    }

The first example below uses the largerOfTwo method to compare two rectangles. The second example uses it to compare two squares. The largerOfTwo method works in both cases since it is wildcarded to support any subclass of Shape.

        Two<Rectangle> tworr = new Two<>(new Rectangle(2.0, 3.0),
                                         new Rectangle(4.0, 5.0));

        System.out.println(largerOfTwo(tworr));

        Two<Square> twoss = new Two<>(new Square(2.0),
                                     new Square(4.0));

        System.out.println(largerOfTwo(twoss));

OUTPUT:

l=4.0 w=5.0

s=4.0

 

The type of a wildcard can also be restricted to a superclass of a type. Suppose you wanted to write a method that checks if the objects in a Two container are equal. In addition, you wish to restrict this method to support squares, rectangles, or shapes. If the type wildcard is restricted to <? super Square>, then the method supports the SquareRectangleShape, and Object classes.

 

The TwoAreEqual method uses the equals method defined in the Shape class to compare both shapes in container Two.

    public static boolean TwoAreEqual(Two<? super Square> two) 
    {
        boolean result = false;

        if (two.getT1().equals(two.getT2()))
            result = true;

        return result;
    }

The following example creates two rectangles and uses the TwoAreEqual method to test if both rectangles are equal. Even though a Square is passed as the second object in the container, the TwoAreEquals method returns true since a 4x4 rectangle and a square with side = 4 have the same area.

   Two<Rectangle> twors = new Two<>(new Rectangle(4.0, 4.0),
                                    new Square(4.0));

   System.out.println(TwoAreEqual(twors));

 

OUTPUT:

true

 

The type parameter of a generic class is handled polymorphically. Since the container is declared to be Two<Rectangle>, then a Square can be used as an object of the type parameter since Square is a subclass of Rectangle.

 

Note that if the container were declared to be Two<Square>, a compilation error occurs because Rectangle is not a subclass of Square.

   Two<Square> twors = new Two<>(new Rectangle(4.0, 4.0), // ERROR
                                 new Square(4.0));

 

Section 9 - Generic Classes and Inheritance

public class TwoStrings<T> extends Two<T>
{
    public TwoStrings(T arg1, T arg2) 
    { 
        super(arg1, arg2); 
    }

    public T larger()
    {
        if (getT1().toString().compareTo(getT2().toString()) > 0)
            return getT1();

        return getT2();

    }

    @Override public String toString()
    {
        return getT1().toString() + " " + getT2().toString();
    }
}


        TwoStrings<Box> sb = new TwoStrings<>(new Box(1, 2, 3),
                                              new Box(4, 5, 6));

        System.out.println(sb);
        System.out.println(sb.larger());

OUTPUT:

l=1 w=2 h=3 l=4 w=5 h=6

l=4 w=5 h=6

    public static <T> T getT1(Two<T> two)
    {
        return two.getT1();
    }

        Two<Box> b = new Two<>(new Box(3, 2, 1),
                               new Box(6, 5, 4));

        System.out.println(getT1(sb));
        System.out.println(getT1(b));

OUTPUT:

l=1 w=2 h=3

l=3 w=2 h=1

    public static void printT1(Two<Box> two)
    {
        System.out.println(two.getT1());
    }

        printT1(b);  // OK
        Two<String> s = new Two<>("H","W");

        printT1(s);  // ERROR

 

Section 10 - Limitations of Generics

 

There are several limitations in implementing generic classes that are not present in non-generic classes. For example, an object of a generic type cannot be created inside a generic class or method.

class A<T>
{
    T t;

    public A()
    {
        t = new T(); // ERROR
    }
}

class Limitations 
{
    public static <T> void method()
    {
        T t = new T(); // ERROR
    }
}

The object of the generic type should be created outside the generic class or method and passed as a parameter to the constructor or method.

class A<T>
{
    T t;

    public A(T tArg)
    {
        t = tArg; // OK
    }
}
 
class Limitations 
{
    public static <T> void method(T tArg)
    {
        T t = tArg; // OK
    }
}

An array of objects of a generic type cannot be created.

class A<T>
{
    T[] array;

    public A(int size)
    {
        array = new T[size]; // OK
    }
}

The array can be created outside the generic class and passed as a parameter to the constructor, as in the previous example. Alternatively, an ArrayList or other collection can be used in place of the Java array.

class A<T>
{
    ArrayList<T> list;

    public A()
    {
        list = new ArrayList<>(); // OK
    }

    public void add(T t) { list.add(t); } // OK
}

Also, a generic exception class may nor be created.

class GenericException<T> extends Exception // ERROR

 

Section 11 - Type Erasure

 

When the compiler encounters a generic class, it performs all necessary syntax checks, then converts generic types to raw types.

 

The following generics

class E<T>
{
    T t;

    public E(T tArg) { t = tArg; }
    public T getT() { return t; }
}

public class Erasure 
{
    public static void main(String[] args)
    {
        E<String> e = new E(“Hello”);
        String s = e.getT();
    }
}

 

are implemented by the compiler using their raw types as follows: 

class E
{
    Object t;

    public E(Object tArg) { t = tArg; }
    public Object getT() { return t; }
}

public class Erasure 
{
    public static void main(String[] args)
    {
        E e = new E(“Hello”);
        Object s = e.getT();
    }
}