Generics

CSCI 1913 – Introduction to Algorithms, Data Structures, and Program Development
Adriana Picoral

The problem

We want to build our own data structures (e.g., a list, a stack, a queue)

  • A list of Strings
  • A list of Integers
  • A list of Students
  • …

Do we need to write a separate class for every type?

One class per type?

A Box that holds a String item:

public class StringBox {
    private String item;

    public StringBox(String item) { this.item = item; }
    public String getItem() { return item; }
}

A Box that holds an Integer item:

public class IntegerBox {
    private Integer item;

    public IntegerBox(Integer item) { this.item = item; }
    public Integer getItem() { return item; }
}
  • Same code, different type – lots of duplication
  • Every bug fix has to be made in every copy

Polymorphism

What is polymorphism?

Polymorphism (“many forms”): the same code can work with values of different types

Java supports a few kinds of polymorphism:

  • Subtype/inclusion polymorphism: established through class inheritance or interface implementation (method overriding)
  • Ad hoc polymorphism: method overloading (same name, different parameter types)
  • Parametric polymorphism: code written with a type parameter – this is generics

Subtype polymorphism (review)

Shape s = new Square(2.0);
System.out.println(s.area());

s = new Circle(1.0);
System.out.println(s.area());
  • One variable type (Shape), many actual object types
  • Dynamic binding picks the right area() method at runtime

Attempt 1: use Object

Every class in Java inherits from Object, so…

public class ObjectBox {
    private Object item;

    public ObjectBox(Object item) { this.item = item; }
    public Object getItem() { return item; }
}

One class that can hold anything!

The trouble with Object

ObjectBox box = new ObjectBox("hello");

String s = (String) box.getItem();  // need a cast

Integer n = (Integer) box.getItem(); // compiles!
// ClassCastException at runtime
  • We have to cast every time we take something out
  • The compiler can’t check that we’re using the right type
  • Errors show up at runtime instead of compile time

Attempt 2: generics

public class Box<T> {
    private T item;

    public Box(T item) { this.item = item; }
    public T getItem() { return item; }
}
  • One class, works with any (reference) type
  • The compiler knows and checks the type
  • No casting needed

Writing a generic class

Type parameters

public class Box<T> {
    ...
}
  • <T> declares a type parameter named T
  • T is a placeholder for a real type, chosen when the class is used
  • Inside the class, use T anywhere you’d use a type: instance variables, parameters, return types, local variables

Naming conventions

Type parameters are usually a single uppercase letter (but they can be anything, this is just a convention):

  • T – Type
  • E – Element (used by Java collections)
  • K, V – Key and Value (used in maps)
  • N – Number

Using a generic class

Provide the actual type in angle brackets:

Box<String> b1 = new Box<String>("hello");
Box<Integer> b2 = new Box<>(42);  // diamond operator: type inferred

String s = b1.getItem();   // no cast
int n = b2.getItem();      // auto-unboxing

b1 = new Box<>(3.14);      // compile error!

Type arguments must be reference types

Generics only work with objects, not primitive types

Box<int> b = new Box<>(5);      // compile error
Box<Integer> b = new Box<>(5);  // OK

Use the wrapper classes:

Primitive Wrapper
int Integer
double Double
char Character
boolean Boolean

Multiple type parameters

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

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

    public K getKey() { return key; }
    public V getValue() { return value; }
}

Declare all types when instantiating a new object:

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

Example: a generic list

public class MyList<E> {
    private E[] items;
    private int size;

    public MyList() {
        items = (E[]) new Object[10];  // can't do: new E[10]
        size = 0;
    }

    public void add(E item) {
        items[size] = item;   // (ignoring resizing for now)
        size++;
    }

    public E get(int index) {
        return items[index];
    }
}

Why (E[]) new Object[10]?

  • Java does not allow new E[10] – the actual type of E is not known at runtime (type erasure)
  • Workaround: create an Object array and cast it to E[]
  • The compiler gives an “unchecked” warning – this is expected and safe as long as we only put Es into the array

Generic methods

A single method (not part of a generic class) can also have its own type parameter:

public static <T> void printAll(T[] array) {
    for (T item : array) {
        System.out.println(item);
    }
}

The method can then be used with different types:

String[] names = {"Ana", "Bo"};
Integer[] nums = {1, 2, 3};
printAll(names);
printAll(nums);

Bounded type parameters

Sometimes we need T to have certain methods

public static <T extends Comparable<T>> T max(T a, T b) {
    if (a.compareTo(b) > 0) {
        return a;
    }
    return b;
}
  • T extends Comparable<T>: T must be a type that can be compared
  • Now we can call compareTo on T values
  • max("apple", "banana") and max(3, 7) both work

Generic interfaces

Interfaces can be generic too

public interface MyStack<E> {
    void push(E item);
    E pop();
    E peek();
    boolean isEmpty();
}

The type parameter needs to match:

public class ArrayStack<E> implements MyStack<E> {
    ...
}

You’ve already used one: Comparable<T>

Why use generics?

Benefits

  • Code reuse: write a data structure once, use it with any type
  • Type safety: the compiler catches type errors at compile time, not at runtime
  • No casting: cleaner, more readable code
  • Clear intent: MyList<Student> tells the reader exactly what’s inside

Compile time vs. runtime errors

// Without generics
ObjectBox box = new ObjectBox("hi");
Integer n = (Integer) box.getItem(); // crashes when run

// With generics
Box<String> box = new Box<>("hi");
Integer n = box.getItem();           // won't compile

It’s always better to find a bug before the program runs

Java’s collections are generic

ArrayList<String> names = new ArrayList<>();
HashMap<String, Integer> ages = new HashMap<>();
ArrayDeque<Integer> stack = new ArrayDeque<>();
  • Every data structure in java.util uses generics
  • The data structures we build in this class will be generic too

In-class exercise

Write a generic class called Duo<T> that holds two items of the same type, with the following public methods:

  • Constructor that takes two arguments: first and second (both of type T)
  • getFirst() and getSecond() return the items
  • swap() swaps the two items (returns nothing)
  • toString() returns the items in the format (first, second)

Submit your Duo.java file to gradescope

Duo<String> d = new Duo<>("hello", "world");
System.out.println(d);            // (hello, world)
d.swap();
System.out.println(d.getFirst()); // world

Duo<Integer> n = new Duo<>(1, 2);
System.out.println(n);            // (1, 2)

Solution

public class Duo<T> {
    private T first;
    private T second;

    public Duo(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() { return first; }

    public T getSecond() { return second; }

    public void swap() {
        T temp = first;
        first = second;
        second = temp;
    }

    public String toString() {
        return "(" + first + ", " + second + ")";
    }
}

Note the local variable temp has type T

Summary

  • Polymorphism: same code works with many types
  • Generics are parametric polymorphism: classes and methods with type parameters (<T>)
  • Type arguments must be reference types (use wrapper classes, capitalized first letter: String, Integer, Boolean, etc.)
  • Can’t create new T[] – use (T[]) new Object[n]
  • Bounded types (<T extends Comparable<T>>) let us require certain methods
  • Generics give us reuse and type safety