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.
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.
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.
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.
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;
}
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();
}
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.
}
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.
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;
}
}
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;
}
Here's a (near) complete implementation of Rational: