Example: Canonical Form for Value Objects

Value Objects versus Reference Objects

Simply put, the fields of a value object (declared or inherited) can't be modified, while one or more fields of a reference object can be modified.

A bank account is a reference object because its balance field is modified every time its withdraw or deposit methods are executed:

class Account {
   private double balance;
   public void withdraw(double amt) { balance -= amt; }
   public void deposit(double amt) { balance += amt; }
   // etc.
}

Obviously an object containing no fields is a value object:

class Calculator {
   public double sin(double x) { ... }
   public double cos(double x) { ... }
   // etc.
}

Often objects representing mathematical objects such as numbers, shapes, and formulae are value objects.

For example:

class Rational {
   private int numerator;
   private int denominator;
   // eetc.
}

If no setter methods are provided for the numerator and denominator fields, then these fields can't change their values after they have been initialized.

The Synchronization Problem

One problem with reference objects is that if two or more of them represent the same real object, then there is a danger that the fields of one could be updated, but not the other. This leads to a synchronization problem.

For example if two account objects represent the same real account, then it is possible that their balance fields could get out of synch with each other.

Value objects don't have this problem. We can have multiple instances of Rational representing the number 1/2. They can't get out of synch with each other because their fields never change.

State vs. Identity

The identity of an object is the real object that it represents. The state of an object is the values of its fields taken together.

Is the identity of an object more than its state or the same?

identity = state OR identity > state?

Generally speaking,, the identity of a reference object is more than its state, while the identity of a value object is the same as its state:

Refence => identity > state

Value => identity = state

For example, two accounts can have the same balance, even though they represent different real accounts.

On the other hand, two rational numbers with the same state (i.e., the same numerators and denominators) represent the same real rational number.

Logical vs. Literal Equality

Two objects are literally equal if they have the same address (i.e., they are literally the same object in the computer's memory).

Java's == operator tests for literal equality:

Account a = new Account(), b = new Account(), c = a;
a == b; // false
a == c; // true

The equals method inherited from the Object class also tests for literal equality:

a.equals(b); // false
a.equals(c); // true

If we disallow multiple reference objects representing the same real object, then we can say that two objects that are literally equal represent the same real object.

However, two value objects can be equal even if they are not literally equal.

For example:

Rational r1 = new Rational(1, 2), r2 = new Rational(1, 2);
r1 == r2; // false, r1 & r2 not literally equal

However, two rationals are equal if they have the same numerator and denominator.

Generally speaking, two value objects are logically equal if they have the same state.

Overriding equals

The canonical form overrides the inherited equals method, implementing it as logical equality instead of literal equality:

   public boolean equals(Object other) {
      if (other == null) return false;
      Class otherClass = other.getClass();
      Class thisClass = Rational.class;
      if (!otherClass.equals(thisClass)) return false;
      Rational otherRat = (Rational)other;
      return otherRat.numerator == this.numerator
             &&
             otherRat.denominator == this.denominator;
   }

Overriding hashCode

Every Java object has a hash code, which is given by the hashCode method inherited from the Object class.

Two objects can have the same hash code. The only rule is:

a.equals(b) => a.hashCode() == b.hashCode()

Hash codes are used by hash tables. A hash table is an array of lists. The lists are called buckets:

class HashTable {
   private List[] buckets;
   // etc.
}

The hash code of an object determines the index of the bucket where it will be stored.

void put(Object x) {
   buckets[x.hashCode()].add(x);
}

Searching a hash table for an object is efficient, because we only need to search a single bucket:

boolean contains(Object x) {
   return buckets[x.hashCode()].contains(x);
}

Of course the search becomes less efficient if most of the stored elements have the same hash code.

Suppose we store a rational number in a hash table:

Rational rat1 = new Rational(1, 2);
someHash.put(rat1);

Later we want to know if 1/2 is stored in this table:

Rational rat2 = new Rational(1, 2);
someHash.contains(rat2);  // may return false

This is possible because we have done nothing to guarantee that rat1 and rat2 have the same hash code, even though they are logically equal.

Here's a quick fix:

class Rational {
   public int hashCode() { return 1; }
   // etc.
}

Now all rational have the same hash code, so trivially:

rat1.equals(rat2) => rat1.hashCode() == rat2.hashCode()

Note that the reverse implication is not required to hold.

Unfortunately, all rationals stored in our hash table will be stored in a single bucket, increasing the average search time from O(1) to O(n).

How to write a hash code function that evenly distributes the hash codes? It's tough to do. I'll bet the hash code functions for Java's library classes are pretty good.

Suppose we have followed canonical form and implemented a toString method for RationaL:

   public String toString() {
      String result = "";
      result += numerator;
      result += '/';
      result += denominator;
      return result;
   }

Note that

rat1.equals(rat2) <=> rat1.toString().equals(rat2.toString())

3In other words, two rationals are logically equal if and only if they look the same.

here's our new implementation of hashCode:

int hashCode() {
   return this.toString().hashCode();
}

Implemented Interfaces

Do we want to be able to save rationals to files?

Do we want to be able to compare two rationals?

Do we want to allow users to make multiple copies of rationals?

If yes, then we must implement the Serializable, Comparable, and Cloneable interfaces:

class Rational implements Serializable, Comparable<Rational>, Cloneable {
   private static final long serialVersionUID = 1L;
   public int compareTo(Rational other) {
      ???
   }
   public
Object clone() throws CloneNotSupportedException {
      ???
   }
   // etc.
}

Obsolescence Problem

An obsolete object is an object created by an earlier version of an application, then stored in a file. When a newer version of the application attempts to read the object, problems can occur as the fields of the object's class may have changed.

By keeping track of the version ID of every class:

private static final long serialVersionUID = 1L;

We can get the JVM to throw an exception when users attempt to load an obsolete object.

Clonability

The Object class implements a clone method. It has two problems: it's protected and it only makes shallow copies.

We can overcome the first problem by overriding it with a public method:

   public Object clone() throws CloneNotSupportedException {
      return super.clone();
   }

To make a deep copy, we need to recursively clone all of the fields:

class Person implements Cloneable {
   private String name;
   private Person spouse;
   public Object clone() throws CloneNotSupportedException {
      Person result = new Person();
      result.name = name.clone();
      result.spouse = spouse.clone();
      return result;
   }
}

Comparability

Suppose rat1 is a/b and rat2 is c/d. Then:

rat1.compareTo(rat2) > 0 => rat1 > rat2
rat1.compareTo(rat2) = 0 => rat1 = rat2
rat1.compareTo(rat2) < 0 => rat1 < rat2

Observe that

a/b >= c/d <=> a*d >= b * c <=> a*d – b*c >= 0

This leads to the following implementation:

   public int compareTo(Rational other) {
      int term1 = this.getNumerator() * other.denominator;
      int term2 = other.getNumerator() * this.denominator;
      return term1 - term2;
   }

Rational.java

Here's a (near) complete implementation of Rational:

Rational.java