Discussion 6: Generics

Solutions

Exercise 1: Other Types of Pairs
(a)

Alice’s physicist friend Bob writes a lot of code performing calculations on his own ComplexNumber type. He’d like to use Alice’s pair classes to model complex coordinates within his simulations. Explain why he won’t be able to do this.

Alice has only implemented pair classes that can store ints and doubles, since ComplexNumber is neither of these, Bob can't use either of these existing implementations.
(b)

What shortcoming does this suggest in Alice’s approach to modeling pairs?

Alice's approach to modeling pairs is inflexible and can't handle storing types other than what she hard-codes a pair implementation to be able to hold. Additionally, Alice's approach involves a lot of repeated code. The IntPair and DoublePair classes are essentially the same, just with different types declared for the fields, parameters, and return values.
(c)

Rather than writing his own ComplexPair class, Bob decides to write a more general Pair class that can store anything in its two entries; its constructor accepts two Objects:

1
2
3
4
5
public class Pair {
  // fields
  public Pair(Object first, Object second) { ... }
  // `first()`, `second()`, `setFirst()`, `setSecond()` methods.
}
1
2
3
4
5
public class Pair {
  // fields
  public Pair(Object first, Object second) { ... }
  // `first()`, `second()`, `setFirst()`, `setSecond()` methods.
}

Bob reasons that he can pass in two ComplexNumbers as the constructor arguments (after all, Object is a supertype of ComplexNumber), so he’ll be able to use Pairs in his simulation. Moreover, Alice can take advantage of auto-boxing to use Bob’s Pair class to model pairs of Integers and Doubles.

Identify two issues with Bob’s approach.

Here are three potential issues:
  1. The return types of the first() and second() methods in Bob's implementation will need to be Object; using any subtype would eliminate the flexibility that Bob is after. However, this means that the static type of these method calls will be Object, and the Compile Time Reference Rule will prevent us from doing most things we'd like to do with the pair entries.
  2. To get around this, we'd need to introduce a lot of explicit casting into our code, which is cumbersome and leaves us vulnerable ClassCastExceptions.
  3. If both fields of the constructor are Object, then Java has no way of enforcing that the two arguments to the constructor have the same (more specific) type. Alice may want to create a pair with two Integers, but accidentally create one with one Integer and one String without any warning.
Exercise 2: Generic Pairs
(a)

Write a generic Pair<T> class that models a pair whose entries both have type T. Your definition should provide the first(), second(), setFirst(), and setSecond() methods as described above. Make a note of all the different ways that your code utilized the generic type T parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Pair<T> {
  /** The first element of a pair: (first, _) */
  T first; 
  /** The second element of a pair: (_, second) */
  T second;

  /** Constructs a pair with the given `first` and `second` elements. */
  public Pair(T first, T second) {
    this.first = first;
    this.second = second;
  }

  /** Returns the first element of the pair. */
  public T first() {
    return this.first;
  }

  /** Returns the second element of the pair. */
  public T second() {
    return this.second;
  }

  /** Replaces the first element of the pair with `newFirst`. */
  public void setFirst(T newFirst) {
    this.first = newFirst;
  }

  /**  Replaces the second element of the pair with `newSecond`. */
  public void setSecond(T newSecond) {
    this.second = newSecond;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Pair<T> {
  /** The first element of a pair: (first, _) */
  T first; 
  /** The second element of a pair: (_, second) */
  T second;

  /** Constructs a pair with the given `first` and `second` elements. */
  public Pair(T first, T second) {
    this.first = first;
    this.second = second;
  }

  /** Returns the first element of the pair. */
  public T first() {
    return this.first;
  }

  /** Returns the second element of the pair. */
  public T second() {
    return this.second;
  }

  /** Replaces the first element of the pair with `newFirst`. */
  public void setFirst(T newFirst) {
    this.first = newFirst;
  }

  /**  Replaces the second element of the pair with `newSecond`. */
  public void setSecond(T newSecond) {
    this.second = newSecond;
  }
}
(b)

Write an AsymPair class generic on two type parameters T1 and T2 modeling the types of its entries (that is, the class declaration should have the form class AsymPair<T1,T2>). Your definition should provide the first(), second(), setFirst(), and setSecond() methods as described above. Make sure you are careful about where you use each generic type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AsymPair<T1,T2> {
  /** The first element of a pair: (first, _) */
  T1 first; 
  /** The second element of a pair: (_, second) */
  T2 second;

  /** Constructs a pair with the given `first` and `second` elements. */
  public AsymPair(T1 first, T2 second) {
    this.first = first;
    this.second = second;
  }

  /** Returns the first element of the pair. */
  public T1 first() {
    return this.first;
  }

  /** Returns the second element of the pair. */
  public T2 second() {
    return this.second;
  }

  /** Replaces the first element of the pair with `newFirst`. */
  public void setFirst(T1 newFirst) {
    this.first = newFirst;
  }

  /**  Replaces the second element of the pair with `newSecond`. */
  public void setSecond(T2 newSecond) {
    this.second = newSecond;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AsymPair<T1,T2> {
  /** The first element of a pair: (first, _) */
  T1 first; 
  /** The second element of a pair: (_, second) */
  T2 second;

  /** Constructs a pair with the given `first` and `second` elements. */
  public AsymPair(T1 first, T2 second) {
    this.first = first;
    this.second = second;
  }

  /** Returns the first element of the pair. */
  public T1 first() {
    return this.first;
  }

  /** Returns the second element of the pair. */
  public T2 second() {
    return this.second;
  }

  /** Replaces the first element of the pair with `newFirst`. */
  public void setFirst(T1 newFirst) {
    this.first = newFirst;
  }

  /**  Replaces the second element of the pair with `newSecond`. */
  public void setSecond(T2 newSecond) {
    this.second = newSecond;
  }
}
(c)

The AsymPair class that you just wrote is a more general type than the Pair class. Use an inheritance relationship with the AsymPair class to redefine the Pair class (while still enforcing that its coordinates both have type T). You should be able to accomplish this with minimal code.

1
2
3
4
5
6
public class Pair<T> extends AsymPair<T,T> {
  /** Constructs a pair with the given `first` and `second` elements. */
	public Pair(T first, T second) {
		super(first, second);
	}
}
1
2
3
4
5
6
public class Pair<T> extends AsymPair<T,T> {
  /** Constructs a pair with the given `first` and `second` elements. */
	public Pair(T first, T second) {
		super(first, second);
	}
}
Exercise 3: Generics and Subtypes
(a)
1
2
3
Quarterback ja = new Quarterback("Josh Allen", "Buffalo");
TightEnd dk = new TightEnd("Dalton Kincaid", "Buffalo");
AsymPair<Quarterback, TightEnd> bills = new AsymPair<>(ja, dk);
1
2
3
Quarterback ja = new Quarterback("Josh Allen", "Buffalo");
TightEnd dk = new TightEnd("Dalton Kincaid", "Buffalo");
AsymPair<Quarterback, TightEnd> bills = new AsymPair<>(ja, dk);
This code compiles. bills's first element must be a Quarterback and its second element must be a TightEnd, and ja and dk match the types in the correct order.
(b)
1
2
3
Quarterback sd = new Quarterback("Sam Darnold", "Seattle");
TightEnd ab = new TightEnd("AJ Barner", "Seattle");
AsymPair<TightEnd, Quarterback> seahawks = new AsymPair<>(sd, ab);
1
2
3
Quarterback sd = new Quarterback("Sam Darnold", "Seattle");
TightEnd ab = new TightEnd("AJ Barner", "Seattle");
AsymPair<TightEnd, Quarterback> seahawks = new AsymPair<>(sd, ab);
This code doesn't compile. seahawks's first element must be a TightEnd and its second element must be a Quarterback. Even though its constructor is being passed a TightEnd and a Quarterback, they're in the wrong order, AsymPair cannot store a Quarterback where it's expecting a TightEnd and vice versa.
(c)
1
2
3
Quarterback dm = new Quarterback("Drake Maye", "New England"); // Drake "Drake Maye" Maye
TightEnd hh = new TightEnd("Hunter Henry", "New England");
AsymPair<Player, Player> patriots = new AsymPair<>(dm, hh);
1
2
3
Quarterback dm = new Quarterback("Drake Maye", "New England"); // Drake "Drake Maye" Maye
TightEnd hh = new TightEnd("Hunter Henry", "New England");
AsymPair<Player, Player> patriots = new AsymPair<>(dm, hh);
This code compiles. patriots is expecting both of its elements to be Players. Even though dm is specifically a Quarterback and hh is specifically a TightEnd, these classes are both subtypes of the Player class. Java can automatically upcast them to work with the pair; this is an instance of the parameter subtype substitution rule.
(d)
1
2
3
Player lj = new Quarterback("Lamar Jackson", "Baltimore");
Player ma = new TightEnd("Mark Andrews", "Baltimore");
AsymPair<Quarterback, TightEnd> ravens = new AsymPair<>(lj, ma);
1
2
3
Player lj = new Quarterback("Lamar Jackson", "Baltimore");
Player ma = new TightEnd("Mark Andrews", "Baltimore");
AsymPair<Quarterback, TightEnd> ravens = new AsymPair<>(lj, ma);
This code doesn't compile. Even though lj's dynamic type is Quarterback and ma's dynamic type is TightEnd, they are both statically typed to be Players. Since ravens is specifically looking for objects that are statically typed to be a Quarterback and TightEnd in that order, and Java doesn't automatically downcast, ravens cannot accept the parameters passed into its constructor.

We could create this pair using explicit casting:

1
AsymPair<Quarterback, TightEnd> ravens = new AsymPair<>((Quarterback) lj, (TightEnd) ma);
1
AsymPair<Quarterback, TightEnd> ravens = new AsymPair<>((Quarterback) lj, (TightEnd) ma);
(e)
1
2
3
4
Quarterback cw = new Quarterback("Caleb Williams", "Chicago");
TightEnd ck = new TightEnd("Cole Kmet", "Chicago");
AsymPair<Quarterback, TightEnd> bears = new AsymPair<>(cw, ck);
AsymPair<Player, Player> players = bears;
1
2
3
4
Quarterback cw = new Quarterback("Caleb Williams", "Chicago");
TightEnd ck = new TightEnd("Cole Kmet", "Chicago");
AsymPair<Quarterback, TightEnd> bears = new AsymPair<>(cw, ck);
AsymPair<Player, Player> players = bears;
This code doesn't compile. All of the code up to and including the creation of bears compiles; however, the fourth line will not. For this assignment statement to work, we'd need the static type of bears to be a subtype of the AsymPair<Player, Player>, but this is not the case. The type AsymPair<Player, Player> supports a setFirst() method that can accept any Player as its argument. The type AsymPair<Quarterback, TightEnd> cannot do this; its setFirst() method can only accept a Quarterback as this argument. Concretely, it must be valid to call players.setFirst(new TightEnd("Colston Loveland", "Chicago")) - however, it would be invalid to call bears.setFirst(new TightEnd("Colston Loveland", "Chicago")). Thus AsymPair<Quarterback, TightEnd> is missing functionality that would be allowed in AsymPair<Player, Player>, so the compiler does not allow this assignment.