Algorithms and OOD (CSC 207 2014F) : Readings

Java Generics


Summary: We consider the mechanisms in Java for creating “generic” classes, classes that can work with a variety of known types of values

Prerequisites: Classes, Interfaces, Polymorphism.

Introduction

When programmers start to design structures that collect values, they hit one immediate design problem: Should the structure contain be homogeneous, and contain only one type of values, or should the structure be heterogeneous and contain multiple types of values? There are many situations in which we want homogeneous data types - e.g., not just any list, but a list of strings or a list of integers or ...

But, once you've decided to have homogeneous data types, how do we write general data types. Once we've implemented all of the methods for a list of strings, the implementation for a list of integers or a list of people or a list of almost anything should be essentially the same.

And now we hit an important language design issue: How does the language allow the programmer to design generalized homogenous collections so that we can indicate the type of value in the collection? The typical strategy is to allow programmers to design collections (classes, interfaces) that have a type parameter. Such parameterized classes and interfaces are typically called generics. We'll consider how to write generics in Java.

An Example: Boxed Values

As you've noted, when two variables refer to the same mutable object, we can change the object through one variable and see the effect through the other variable. At times, that effect is undesirable, but at others it is desirable. For example, you might want changes to invidual banki accounts to also affect the bank's total cash on hand.

Unfortunately, if the two variables refer to the same immutable object, such as a string, we can't propagate the change to one variable to the other variable. The typical solution to this problem is to have what is typically called a “box”. Variables are very simple objects with two primary methods: You can set the value in a box or get the value in a box.

For example, here's a session with a boxed string class.

  BoxedString s1 = new BoxedString("Hello");
  BoxedString s2 = s1;
  pen.println(s1.get());        // Prints "Hello"
  pen.println(s2.get());        // Prints "Hello"
  s1.set("Goodbye");
  pen.println(s1.get());        // Prints "Goodbye"
  pen.println(s2.get());        // Prints "Goodbye"
  s2.set("Whatever");
  pen.println(s1.get());        // Prints "Whatever"
  pen.println(s2.get());        // Prints "Whatever"

As you might guess, implementing boxed strings is relatively straightforward.

/**
 * Boxes that hold strings.
 */
public class BoxedString
{
  // +--------+------------------------------------------------------
  // | Fields |
  // +--------+

  /**
   * The contents of the box.
   */
  String contents;

  // +--------------+------------------------------------------------
  // | Constructors |
  // +--------------+

  /**
   * Build a new box with specified initial contents.
   */
  public BoxedString(String initialContents)
  {
    this.contents = initialContents;
  } // BoxedString(String)

  // +---------+-----------------------------------------------------
  // | Methods |
  // +---------+

  /**
   * Get the contents of the box.
   */
  public String get()
  {
    return this.contents;
  } // get()

  /**
   * Update the contents of the box.
   */
  public void set(String newContents)
  { 
    this.contents = newContents;
  } // set(String)

  /**
   * Get the contents of the box as a string.
   */
  public String toString()
  {
    return this.contents;
  } // toString()
} // class BoxedString

That's pretty straightfoward, right?

What happens when we want to store something else, such as BigInteger values? One option is that we could copy the code and then do a search and replace to change every instance of String to BigInteger. But, as you know, copy-paste-change is rarely a good strategy. For example, a pure copy-paste-replace will wreak with the toString method. And what if we had another String field, such as one that stores the name of the box? Then we'd need to look at each instance of the word String separately.

Shouldn't it be easier to make a box that can work for String values or BigInteger values or ...? What do we do? Wouldn't you expect to have a solution to what seems to be a common problem?

There is one obvious solution. Instead of saying that we store String values or BigInteger values or whatever in a box, we can just store Object values.

    Object contents;
    ...
    Object get()
    {
      return this.contents;
    } // get()
    ...
    Object set(Object newContents)
    {
      this.contents = newContents;
    } // set(Object)

As you might guess, there are significant downsides to this approach. For example, Java assumes that everything in a box is an objects, so we have to cast. That is, the following code doesn't work.

  Box i1 = new Box(new Integer(42));
  pen.println(i1.get() * 2);    // Compile error: Can't multiply objects

So we have to write something like the following.

  Box i1 = new Box(new Integer(42));
  pen.println(((Integer) i1.get()) * 2);

And, as you know, skipping Java's compile-time type checking for run-time checking when you cast can be dangerous.

Can we do better? Can we retain Java's strengths of compile-time typechecking while also writing more general code? That's where Java generics enter the picture. In essence, we want to say that we have a box that holds values of some type, which we'll call T. The get method returns a value of type T and the set method takes as a parameter a value of type T. In effect, T. In Java, we can use type variables if we follow the class name with a less-than sign, the type variable, and a greater-than sign.

Here's our BoxedString class, rewritten as a generic class.

/**
 * Generic boxes hold shared values.
 */
public class Box<T>
{
  // +--------+------------------------------------------------------
  // | Fields |
  // +--------+

  /**
   * The contents of the box.
   */
  T contents;

  // +--------------+------------------------------------------------
  // | Constructors |
  // +--------------+

  /**
   * Build a new box with specified initial contents.
   */
  public Box(T initialContents)
  {
    this.contents = initialContents;
  } // Box(String)

  // +---------+-----------------------------------------------------
  // | Methods |
  // +---------+

  /**
   * Get the contents of the box.
   */
  public T get()
  {
    return this.contents;
  } // get()

  /**
   * Update the contents of the box.
   */
  public void set(T newContents)
  { 
    this.contents = newContents;
  } // set(T)

  /**
   * Get the contents of the box as a string.
   */
  public String toString()
  {
    return this.contents.toString();
  } // toString()
} // class Box<T>

How do we use these new generic classes? Much like we use normal classes, except that we must specify a type parameter when we declare or create them.

  Box<String> s1 = new Box<String>("Hello");
  Box<String> s2 = s1;
  pen.println(s1.get());        // Prints "Hello"
  pen.println(s2.get());        // Prints "Hello"
  s1.set("Goodbye");
  pen.println(s1.get());        // Prints "Goodbye"
  pen.println(s2.get());        // Prints "Goodbye"
  s2.set("Whatever");
  pen.println(s1.get());        // Prints "Whatever"
  pen.println(s2.get());        // Prints "Whatever"

  Box<Integer> i1 = new Box<Integer>(42);
  Box<Integer> i2 = i1;
  pen.println(i2.get() + 3);          // Prints 45
  ii.set(21);
  pen.println(i2.get() + 2);          // Prints 23

As you might expect, the benefits of generics are much like the benefits of any instance in which you generalize instead of using the copy-paste-change approach If you notice a bug, you only need to correct it in one place. If you want to add a feature, you only need to add it in one place.

Of course, this isn't to say that there are problems with generics. As you use them, you'll find that there are some unexpected complexities. We'll address a few as we continue through this reading.

A More Complex Example: Simple Array-Like Collections

Let's consider another simple example to further ground the design of generics. Let's suppose we were going to evaluate expandable arrays of strings. We'll start with the interface.

/**
 * Expandable arrays of strings.
 */
public interface ExpandableArrayOfStrings
{
  /**
   * Set the ith element of the array to str.
   *
   * @pre i >= 0
   * @post get(i) = str
   */
  public void set(int i, String str);

  /**
   * Get the ith element of the array.  If the ith element has not
   * been set, returns null.
   *
   * @pre i >= 0
   */
  public String get(int i);
} // interface ExpandableArrayOfStrings

Now, let's see how we might implement that interface. We'll probably have a field that stores the contents.

   /**
    * The strings in the array.
    */
   String[] values;

When we create a new object, we'll initialize that array.

  /**
   * Create a new expandable array.
   */
  public SimpleExpandableArrayOfStrings()
  {
    this.values = new String[16];
  } // SimpleExpandableArrayOfStrings()

To set the ith element of the expandable array, we make sure that the underlying array is big enough. If not, we expand it. We can then set using the normal mechanism for setting values.

  public void set(int i, String str)
  {
    // If the array is not big enough, expand it
    if (this.values.length <= i)
      {
        int newsize = this.values.length * 2;
        while (newsize <= i)
          {
            newsize *= 2;
          } // while
        this.values = Arrays.copyOf(this.values, newsize);
      } // if the array is no big enough
    // And set the values
    this.values[i] = str;
  } // set(int, String)

To get the ith element of the expandable array, we make sure that i is not too big. If it is too big, we just return null.

  public String get(int i)
  {
    // If the array is not big enough, just return null
    if (this.values.length <= i)
      {
        return null;
      } // if the array is not big enough
    // Get the value
    return this.values[i];
  } // get(int)

What happens when we want to store something else, such as BigInteger values? We don't really want to copy, paste, and change the code. What do we do? That's where generics enter the picture again.

public interface ExpandableArray<T>
{
  /**
   * Set the ith element of the array to val.
   *
   * @pre i >= 0
   * @post get(i) = val
   */
  public void set(int i, T val);

  /**
   * Get the ith element of the array.  If the ith element has not
   * been set, returns null.
   *
   * @pre i >= 0
   */
  public T get(int i);
} // interface ExpandableArray<T>

Not much of a change, is it?

But what should we do with the implementation? Essentially, we would hope that all we have to do is replace each instance of String with T.

public class SimpleExpandableArray<T>
  implements ExpandableArray<T>
{
  T[] values;

  public SimpleExpandableArray()
  {
    this.values = new T[16];
  } // SimpleExpandableArray

  public void set(int i, T val)
  {
    // If the array is not big enough, expand it
    if (this.values.length <= i)
      {
        int newsize = this.values.length * 2;
        while (newsize <= i)
          {
            newsize *= 2;
          } // while
        this.values = Arrays.copyOf(this.values, newsize);
      } // if the array is no big enough
    // And set the values
    this.values[i] = val;
  } // set(int, T)

  public T get(int i)
  {
    // If the array is not big enough, just return null
    if (this.values.length <= i)
      {
        return null;
      } // if the array is not big enough
    // Get the value
    return this.values[i];
  } // get(int)
} // class SimpleExpandableArray<T>

Unfortunately, life isn't quite that simple. There are some complex typing issues that correspond to making arrays of a generic type. Instead, Java requires us to create an array of objects and cast it to the appropriate type. But that cast is unsafe, so Java also requires us to say that we know it's unsafe. Here's what we have to write instead.

  @SuppressWarnings({"unchecked"})
  public SimpleExpandableArray()
  {
    this.values = (T[]) new Object[16];
  } // SimpleExpandableArray

Once we've created the generic class, we can create objects in that class as we would normally, except that we provide a type to the constructor.

  ExpandableArray<String> strings = 
      new SimpleExpandableArray<String>();
  ExpandableArray<BigInteger> numbers = 
      new SimpleExpandableArray<BigInteger>();

Polymorphism and Generic Boxes

In the previous section, we saw that Java is less nice than we'd like it to be about arrays of generic types. You can look online to find some details as to why. To help you understand potential problems, let's consider a small issue of polymorphism and generics.

As you've found, every Integer is an Object. This means that we can write things like the following:

  Integer i  = new Integer(42);
  Object o = i;

Can we use the same idea for generics? Let's think about it. Here's an apparent generalization of the previous example.

  Box<Integer> ibox = new Box<Integer>(new Integer(42));
  Box<Object> obox = ibox;

That seems harmless enough, doesn't it? But it turns out that it's not. In fact, it's quite dangerous. Consider what the Java compiler thinks about the following:

  obox.set("Hello");

Is that legal? It seems to be. obox holds objects. "Hello" is a string. Strings are objects.

But we also think of the box as holding integers. So what happens when we next write something like the following?

  pen.println(ibox.get() * 2);

Well, the box currently has a string in it. We can't multiple strings and integers. (And even before that, having ibox.get() return a string violates the signature of Box<Integer>.get. Boom!

To prevent errors like this, the Java compiler is very restrictive in how it lets you assign objects that belong to generic classes.

Note that errors like this reinforce the main reason we don't just achieve generic structures by using Object everywhere. When we do so, we move type checking from a careful, program-driven, compile-time activity to a run-time activity that relies on the good will of programmers to use things in the way they say they will.

An Example: Predicates

When else might we use generics? Here's another simple generic class. Sometimes we want to be able to apply a true/false function to an object, such as when we're searching for a value that meets a certain criterion.

/**
 * A simple predicate.
 */
public interface Predicate<T>
{
  public boolean holds(T val);
} // Predicate<T>

We'll see how to use predicate objects in the next section.

Generic Methods

We've seen how to make generic interfaces and generic classes. Can we also make generic functions? Yes, you may have already seen one. We can write static functions that work with a variety of types, but do appropriate type checking. In this case, you put the type variable immediately after the static declaration. For example, given an array of values, we can search it with the following.

  /**
   * Return the first value in vals for which the pred holds.
   * Return null if nothing is found.
   */
  public static <T> T search(T[] vals, Predicate<T> pred)
  {
    for (int i = 0; i < vals.length; i++)
      {
        if (pred.holds(vals[i]))
          return vals[i];
      } // for
    return null;
  } // search(T[], Predicate<T>)

We'll see how to use this approach in the corresponding lab.

Dealing with Multiple Type Variables

We've seen how to parameterize a class definition with one type. Can we do it with more than one type? Yes. Here's a very simple class that can hold two kinds of values.

public class Pair<T,U>
{
  T val1;
  T val2;
} // class Pair<T,U>

We can now create objects that hold two types of values, but know what kinds of values they can hold.

  Pair<BigInteger,String> foo = new Pair<BigInteger,String>();
  Pair<BigInteger,BigInteger> bar = new Pair<BigInteger,BigInteger>();

We'll see some interesting applications of multi-parameterized generics in the near future.