Generics

CSC-430

Phillip Wright

Problem

Suppose we want to create a pair data structure which can store elements of two specific types. In older versions of Java, there were only two options:

  • Define multiple pair classes for different type combos
  • Define a pair class that can store any types

With Specific Types

public class StringAndImagePair {
  private final String a;
  private final Image b;
  //...
  public String a(){
    return a;
  }

  public Image b(){
    return b;
  }
}

Problem II

This works, but it goes against the general goals we have of writing reusable code, because we have to create a new definition with almost identical code for every pair of types we want to support.

Untyped Data

public class Pair(){
  private final Object first;
  private final Object second;
  //...
  public Object first(){
    return first;
  } 

  public Object second(){
    return second;
  }
}

Problem III

This class can be reused for any two types we want, but this code is very dangerous, because it doesn’t effectively capture the types used in the pair.

Problem IV

public void iNeedAPairOfStrings(Pair pairOfStrings){
  // Fails at runtime with ClassCastException!
  final String first = (String) pairOfStrings.first();
  final String second = (String) pairOfStrings.second();
  //...
}

// oops!
iNeedAPairOfStrings(new Pair(someArray, someImage));

Problem V

We must check types to make sure the Pair is used safely.

public void iNeedAPairOfStrings(Pair pairOfStrings){
  if(pairOfStrings.first() instanceof String &&
      pairOFStrings.second() instanceof String){
    //...
  }
}

Generics

To resolve this issue, “Generics” were added to Java, which allows for types to be defined with type parameters which specify the concrete types referenced in generic data structure definitions.

Generic Pair

public class Pair<T,U>{
  //...
}

We define a generic Pair class which accepts two type parameters. We can then instantiate different types using this single definition.

Pair<String,Image> stringAndImage = new Pair<>(someString, someImage);
Pair<String,String> pairOfStrings = new Pair<>("hello","world");

Generic Pair II

public void iNeedAPairOfStrings(Pair<String,String> pair){
  //...
}

Now…

// works fine!
iNeedAPairOfStrings(pairOfStrings);
// won't compile!
iNeedAPairOfStrings(stringAndImage);

Generic Pair III

public class Pair<T,U>{
  private final T t;
  private final U u;
  public Pair(T t, U u){
    this.t = t;
    this.u = u;
  }
  public T first(){
    return t;
  }
  public U second(){
    return u;
  }
}

Methods

public class Pair<T,U>{
  //...
  public <V> Pair<V,U> mapFirst(Function<T,V> f){
    return new Pair<>(f.apply(t),u);
  }

  public <V> Pair<T,V> mapSecond(Function<U,V> f){
    return new Pair<>(t,f.apply(u));
  }
}

Methods II

public class Pair<T,U>{
  //...
  public <V,W> Pair<V,W> map(Function<T,V> f, Function<U,W> g){
    final V first = f.apply(t);
    final W second = g.apply(u);
    return new Pair<>(first,second);
  }
}

Wildcards

What if we want to write a method that accepts a list of any kind of number?

You might think List<Number> would work for a List<Integer>, but it doesn’t! List<Integer> is not a subtype of List<Number>.

Wildcards II

To handle this, Java uses wildcards (?).

  • ? extends Type: An upper-bounded wildcard.
  • ? super Type: A lower-bounded wildcard.

Wildcard Example

Using a wildcard, we can now write a flexible sum method.

public double sum(List<? extends Number> list) {
  double sum = 0.0;
  for (Number n : list) {
    sum += n.doubleValue();
  }
  return sum;
}

Type Erasure

A key thing to understand about generics in Java is type erasure.

After the compiler checks your code for type-safety, it erases the generic type information. In the compiled bytecode, your Pair<String, Image> is just a Pair, and the fields are just Objects.

This was done for backward compatibility with pre-generics Java.

Erasure Example

Because of type erasure, you can’t do certain things. For example, you can’t check the specific generic type at runtime:

Pair<String, String> pair = new Pair<>("a", "b");

// This will produce a compile-time warning!
if (pair instanceof Pair<String, String>) {
  // ...
}

// You can only check the raw type
if (pair instanceof Pair) { // This is allowed
  // ...
}

Type Erasure II

You also can’t create new instances of a generic type parameter, like new T().