Small introduction to Java Generics

January 21, 2021



Yesterday I had to lecture a class on Java Generics. These are some of the introductory material I gave them.

Generic methods

To show one use-case of generics (generic methods), I started by typing a public static void show(int value) method which just printed an integer on the console. I instructed my students to do the same as I believe that they learn better by doing. Then I made them add more show() methods to include other data types. Here's the code:

public class Main {

    public static void show(int value) {
        System.out.println(value);
    }

    public static void show(float value) {
        System.out.println(value);
    }

    public static void show(char value) {
        System.out.println(value);
    }

    public static void show(boolean value) {
        System.out.println(value);
    }

    public static void show(String value) {
        System.out.println(value);
    }

    // ...

    public static void main(String[] args) {
        int int_value = 5;
        double float_value = 5.5;
        char char_value = 'C';
        boolean boolean_value = true;
        String string_value = "hello world!";
        byte byte_value = 0xA;
        int[] int_array_value = new int[] {1, 2, 3};

        show(int_value);
    }
}

Then I instructed them that the output (for all methods) should be something like "The value is ..". Eventually they all got the point that it was unsustainable, so I introduced generic types:

public class Main {

    public static <T> void show(T value) {
        System.out.println("The value is " + value);
    }

    public static void main(String[] args) {
        int int_value = 5;
        double float_value = 5.5;
        char char_value = 'C';
        boolean boolean_value = true;
        String string_value = "hello world!";
        byte byte_value = 0xA;
        int[] int_array_value = new int[] {1, 2, 3};

        show(int_value);
    }
}

Now you only need to change things once. That first lesson was well understood!

Compile time checks

To show that some errors can happen at runtime, I made the students copy the following example:

class Box {
    private Object obj;

    public Object get() {
        return obj;
    }

    public void set(Object obj) {
        this.obj = obj;
    }
}

public class Main {

    public static void main(String[] args) {
        Box box = new Box();
        box.set(1);
        String value = (String) box.get(); // Runtime error!
    }
}

This generates the following runtime error:

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap') at Main.main(Main.java:18)

By using generic types, the error is now thrown by the compiler itself, as we're creating a box object to store an integer, and trying to convert to a String.

class Box <T> {
    private T obj;

    public T get() {
        return obj;
    }

    public void set(T obj) {
        this.obj = obj;
    }
}

public class Main {

    public static void main(String[] args) {
        Box<Integer> box = new Box<Integer>();
        box.set(1);
        String value = box.get(); // Compiler error - Requires String, got int
    }
}

Less (or no) casts

This was explained using the previous Box class examples. If Box is implemented with Object, the public Object get() method's result must be coerced to integer:

public static void main(String[] args) {
    Box box = new Box();
    box.set(1);
    int value = (int) box.get(); // Must force cast or the compiler will be annoyed..
}

If we use generic types, the compiler will know the type beforehand so we can safely ignore types on the public T get() method:

public static void main(String[] args) {
    Box<Integer> box = new Box<>();
    box.set(1);
    int value = box.get(); // No need to cast!
}

Bounded generics

To explain bounded generics, I made the students copy the following example. The idea is to provide methods to add two numbers, being them int or double:

public class Main {

    public static double add(int a, int b) {
        return 0;
    }

    public static double add(int a, double b) {
        return 0;
    }

    public static double add(double a, int b) {
        return 0;
    }

    public static double add(double a, double b) {
        return 0;
    }

    public static void main(String[] args) {
        System.out.println(add(2, 3.5));
    }
}

As they already knew that I could generalize the add* method, I wrote the following pseudo-generic method:

public static <T> double add(T a, T b) {
    return a + b;
}

This fails with an error such as Operator '+' cannot be applied to T, T. Since the compiler does not know the exact types which can be used, it refuses to add the two variables. I made the point that we should instruct the compiler that we will behave nicely and only provide numbers. This is the resulting method.

public static <T extends Number> double add(T a, T b) {
    return a.doubleValue() + b.doubleValue();
}

The trick is in "T extends Number" that means we will only provide Objects which are subclass of Number. And since the class Number (and all subclasses) provides the doubleValue() method, the '+' operator works fine.

Exercises

As for exercises, I had the students implement a Stack class to store integer. The stack is based on an array of integers (to store the values) and another integer to indicate the index of the top of the stack. Then they had to implement the methods void push(int value) and int pop() taking care of the array bounds. In the end, I asked the students to change the code such that they could store any type of values in the stack, not just integers. Here's the resulting code (without checking array bounds):

class Stack <T> {
    Object[] values;
    int curr;

    public Stack(int size) {
        values = new Object[size];
        curr = 0;
    }

    public void push(T value) {
        values[curr] = value;
        curr = curr + 1;
    }

    public T pop() {
        curr = curr - 1;
        return (T) values[curr];
    }
}

public class Main {
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>(10);
        stack.push("world");
        stack.push("hello");
        System.out.println(stack.pop());
        System.out.println(stack.pop());
    }
}

Done!