The geom package contains classes that represent basic geometric objects. We begin with two-dimensional points:
class Point {
private double xc, yc;
public Point(double xc, double yc) {
this.xc = xc;
this.yc = yc;
}
public Point() { this(0, 0); }
public double getXC() { return xc; }
public double getYC() { return yc; }
public String toString() {
return "(" + xc + ",
" + yc + ")";
}
}
Here is how we might create a few Point objects:
Point p1 = new Point(3, 4), p2 = new Point();
When a point appears in a context where a string is expected, the toString method will automatically be called to convert the point into a string:
System.out.println("p1 = " + p1); // prints "p1 = (3, 4)"
A line segment consists of two points:
class LineSegment {
private Point start, end;
public LineSegment(Point start, Point
end) {
this.start = start;
this.end = end;
}
public Point getStart() { return start;
}
public Point getEnd() { return end; }
public String toString() {
return "[" + start +
", " + end + "]";
}
}
Here is how we create a line segment:
LineSegment seg = new LineSegment(p1, p2);
public class GeomTests {
//
test fixture:
static Point p1 = new Point(3, 4);
static Point p2 = new Point(3, 4);
static Point p3 = new Point(-1,
-2);
static LineSegment l1 = new
LineSegment(p1, p3);
static LineSegment l2 = new
LineSegment(p2, p3);
// tests:
public static void test1() {
System.out.println("test1 ...
");
System.out.println("p1 == p2 =
" + (p1 == p2));
System.out.println("p1.equals(p2)
= " + p1.equals(p2));
System.out.println("l1 = "
+ l1);
System.out.println("l2 = "
+ l2);
}
public static void test2() {
System.out.println("test2 ...
");
Map temps = new Hashtable();
temps.put(p1, new Double(212.0));
temps.put(p3, new Double(98.2));
System.out.println("temps["
+ p2 + "] = " + temps.get(p2));
}
public static void main(String[] args)
{
test1();
test2();
}
}
The result of test1 shows that p1 == p2 and p1.equals(p2) both determine if p1 and p2 are literally the same. The result of test2 shows that the temperature at p1 is not found using the logically equivalent but literally different p2:
test1 ...
p1 == p2 = false
p1.equals(p2) = false
l1 = [(3.0, 4.0), (-1.0, -2.0)]
l2 = [(3.0, 4.0), (-1.0, -2.0)]
test2 ...
temps[(3.0, 4.0)] = null
The fields of an object constitute its state, while the methods constitute its behavior. If the fields can be modified, then we say the object is stateful, otherwise, the object is stateless. For example, points and lines are stateless, while bank accounts are stateful.
The identity of a stateless object is a function of its state, while the identity of a stateful object transcends its state. For example, two points with the same x and y coordinates would be regarded as equal, while two bank accounts with the same balance would not.
Stateless objects tend to be non-modifiable because modifying the internal state of such an object would change its identity.
Domain objects represent entities, roles, descriptions, and events in application domains. We say that the thing represented is the extension of the domain object.
Having multiple domain objects with the same extension can lead to synchronization problems if the domain objects are stateful.
For example, assume:
Account a1 = new Account(), a2 = new Account(), a3 = a1;
If a1 and a2 have the same extension (Joe Smith's checking account), then synchronization problems arise when a1.balance differs from a2.balance.
Having multiple stateless domain objects with the same extension usually doesn't lead to problems. This is reflected in our UML diagram that shows a point object is the end point of at most one line segment.
Fortunately, we can override the equals() method so that it tests for logical rather than physical equality. This is a bit tricky because equals compares this point to all other objects, not just points. We want to be sure that equals returns false when other is not an instance of Point (we even return false when other is an instance of a subclass of Point). To do this we need to use reflection:
class Point {
public boolean equals(Object other) {
if (other == null) return false;
Class c1 = other.getClass();
Class c2 = this.getClass();
if (!c1.equals(c2)) return false;
Point p = (Point)other;
return p.xc == xc && p.yc ==
yc;
}
// etc.
}
class Point {
public boolean equals(Object other) {
if (other == null) return false;
if (! (other instanceof Point))
return false;
Point p = (Point)other;
return p.xc == xc && p.yc ==
yc;
}
// etc.
}
Now equals() tests for logical equality, but we still can't locate the temperature of p1 using the logically equivalent p2:
test1 ...
p1 == p2 = false
p1.equals(p2) = true
l1 = [(3.0, 4.0), (-1.0, -2.0)]
l2 = [(3.0, 4.0), (-1.0, -2.0)]
test2 ...
temps[(3.0, 4.0)] = null
Recall that an open hash table is an array of linked lists called buckets. The indices of this array are called hash codes. Determining if p2 is in temps involves two steps. First, p2.hashCode() determines which bucket to search. Secondly, each element in the associated bucket is compared to p2 using equals().
The problem is that while p1 and p2 are logically the same, p1.hashCode() and p2.hashCode() are different. Therefore, we end up searching the wrong bucket!
To correct this problem we override hashCode() so that logically equal elements have the same hash code. We must be careful, though. Different objects should usually have different hash codes so buckets will tend to have similar lengths.
class Point {
public int hashCode() {
// return 1; this works too, but
it's a bad idea
return toString().hashCode();
}
// etc.
}
test1 ...
p1 == p2 = false
p1.equals(p2) = true
l1 = [(3.0, 4.0), (-1.0, -2.0)]
l2 = [(3.0, 4.0), (-1.0, -2.0)]
test2 ...
temps[(3.0, 4.0)] = 212.0
class LineSegment {
public boolean equals(Object other) {
if (other == null) return false;
Class c1 = other.getClass();
Class c2 = this.getClass();
if (!c1.equals(c2)) return false;
LineSegment ls = (LineSegment)other;
return start.equals(ls.start)
&& end.equals(ls.end) ||
start.equals(ls.end) && end.equals(ls.start);
}
public int hashCode() { return
toString().hashCode(); }
// etc.
}