Lecture 8: Class Invariants
1 Example:   the Even class
2 What’s going on here?
3 Back to Connect $N$
4 The class invariant reasoning principle
5 Example:   rational numbers
6 Other invariants
6.5

Lecture 8: Class Invariants

In Lecture 7 we discussed the idea of bad freedoms—that a representation might be able to do things that don’t make sense in terms of the information that it models. For example, the width field of our proposed Connect $N$ model implementation is declared with type int, which means that as far as Java is concerned, any int goes, even if it’s something like -8, which makes no sense as a width.

Some bad possibilities are ruled out by the language we’re programming in. In Java, if we declare width to be an int then the language guarantees that it will be.1What possibilities does your favorite language let you rule out? However, Java (and most but not all other languages) gives us no way to say directly that width must be positive. But it does give us a way to control it, if we think and program carefully.

1 Example: the Even class

Consider this class for representing even integers:

/**
 * An even integer.
 */
final class Even {
  /*
   * Constructs an {@code Even} from an even {@code int}.
   *
   * @param value  the {@code int} representation of the even number
   * @throws IllegalArgumentException if {@code value} isn't even
   */
  public Even(int value) {
    if (value % 2 != 0) {
      throw IllegalArgumentException("value must be even");
    }

    this.value = value;
  }

  /**
   * Returns the even value as an {@code int}.
   *
   * @return the even {@code int}
   */
  public int getValue() {
    return value;
  }
  private final int value;
}

The Even class has one field, an int representing the number. Given the intended meaning of an Even class, it would be wrong for field value to contain an odd number (or from the client perspective, that the result of getValue() would be odd). Because the Java programming language doesn’t understand, much less track, evenness, it cannot directly enforce this restriction for us. Instead, the Even class enlists other rules of the language to enforce its requirements.

How do we know that field value can never be odd?

Together, these two facts are enough to establish that value is always even, because the constructor makes that so and nothing2Well, nothing within Java, but take a look at JNI.

Let’s consider what happens when the class is modified slightly. Like Even, EvenCounter’s value should always be even, but EvenCounter affords the client to increment the value field:

final class EvenCounter {
  public EvenCounter(int value) {
    if (value % 2 != 0) {
      throw IllegalArgumentException("value must be even");
    }

    this.value = value;
  }

  public int nextValue() {
    return value += 2;
  }
  private int value;
}

Again we’d like ensure that value is always even, but the situation is complicated by mutation.

How do we know? Consider that

That last point is key. Does nextValue() ensure that value is even when it returns? Not at all! However, it does ensure that if value is even when nextValue() is called then value continues to be even upon the method’s return.

We can use similar reasoning to determine whether adding particular methods would allow objects of the class to violate its rules. Consider, for example, these methods:

2 What’s going on here?

In the preceding section, we employed a technique for reasoning about programs called a class invariant. A class invariant is a logical statement about the instantaneous state of an object that is ensured by the constructors and preserved by the methods. Let’s break that down:

Here are some comments that are not class invariants:

3 Back to Connect $N$

We can apply the class invariant technique to our implementation of the Connect $N$ model to rule out the additional kinds of nonsense states that were method earlier, such as dimensions being negative or the columns list containing values that don’t correspond to players. We guard against these possibilities by imposing class invariants and checking that they’re respected. In the case of Connect $N$, we want know that the dimensions are always sensible (positive), the turn stands for a valid player, the length of the columns list equals width of the grid, the length of every column in the list doesn’t exceed height, an all the elements of the columns are non-null integers between 0 and players - 1.

In order to apply class invariant reasoning, we need to determine what invariants we have (or think we have), and then check the code to make sure that’s true.

4 The class invariant reasoning principle

Class invariants enable a form of reasoning called rely-guarantee. The idea is that components rely on some things being true when they’re invoked, and they guarantee some things being true upon return (provide their reliance is satisfied). In particular,

In this way, class invariants allow you to rule out object representations that you don’t want.

5 Example: rational numbers

Sometimes we want to rule out representations because they don’t make sense in terms of the relevant domain, but another reason to restrict representations is to make other parts of our program simpler. For example, we might write rational number class using a fractional representation with a numerator field and a denominator field. Unfortunately, this representation has a wrinkle, because there are many ways to write each rational number as a fraction.

We now consider three iterations of an implementation of a simple Rational interface:

6 Other invariants

The notion of an invariant should be familiar from last year. Recall from Fundies 2 our discussion of heaps, which were binary trees that obeyed two invariants simultaneously:

These invariants gave us similar rely-guarantee reasoning principles as the class invariants we discussed above, albeit based on totally different premises. Note that both of these invariants are instantaneous properties, like class invariants, but they are not properties of a single isolated object (though if you squint and treat the entire heap as a “single” entity, it’s pretty close). We then used these invariants to drive our algorithm design.

Class invariants are similar in spirit, but a bit different in scope. They focus more on making all the methods of a single class work properly in concert, so that other parts of the program can rely on the property being true. When a single class implements an entire data type, class invariants and logical invariants become largely the same thing, but it’s rare for a data type to be implemented by just one class!

1What possibilities does your favorite language let you rule out?

2Well, nothing within Java, but take a look at JNI.