JAVA FAIL

Thursday, December 18, 2008

IS-A FAIL

I knew this was wrong the first time I saw it. After having the chance to work with the java.sql.Timestamp class for a while, I'm just left shaking my head. You can probably figure out what's wrong just by reading the class documentation, but here's a synopsis.

The java.util.Date class is the "normal" timestamp class in the Java standard library. It's basically a wrapped up POSIX timestamp, except that 64-bit signed value is an expression of the number of milliseconds since the epoch instead of the usual seconds. A strange choice to begin with, but not really unreasonable.

Databases may represent timestamps to a higher resolution than milliseconds, in the general case. When interacting with a database with JDBC, one doesn't want to lose precision on timestamp values merely because of Date's chosen resolution. JDBC chose to address this by introducing the Timestamp class, which records times to nanosecond resolution. Presumably since most of the behaviour and and interface on Date also make sense for Timestamp, Timestamp derives from Date. But hold on, take a look at that note on Timestamp:
Note: This type is a composite of a java.util.Date and a separate nanoseconds value. Only integral seconds are stored in the java.util.Date component. The fractional seconds - the nanos - are separate. The Timestamp.equals(Object) method never returns true when passed an object that isn't an instance of java.sql.Timestamp, because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashcode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.

Due to the differences between the Timestamp class and the java.util.Date class mentioned above, it is recommended that code not view Timestamp values generically as an instance of java.util.Date. The inheritance relationship between Timestamp and java.util.Date really denotes implementation inheritance, and not type inheritance.

Woah! So, Timestamp is-not-a Date! This note is essentially a warning to not treat a Timestamp polymorphically as if it were a Date. Now this isn't in itself always necessarily evil. Although, in C++, you might think about using private or protected inheritance with some using declarations, or aggregation. In Java, you're stuck with the choice of aggregation and re-offering a semantically-different-but-syntactically-the-same interface on Timestamp. If a Timestamp is not usable as a Date, then aggregation seems like the right choice here.

But wait a minute. Is there really a need for Timestamp to be unusable as a Date? Why did they chose to stop storing the milliseconds of the timestamp value in the Date part of the object? That decision doesn't really make any sense. Why wouldn't you just leave the Date part of the class as-is and allow it to store whole milliseconds, and then have an additional field that stores the fractional milliseconds (down to the resolution of nanoseconds)?

This wouldn't require any changes in the Timestamp interface at all. The class could still offer getTime() and getNanos() methods and so on cheaply, and, critically, it would then be true that a Timestamp is-a Date! You could compare two Timestamp values through their Date.equals() interface (since the Timestamp version would be called). You could even compare Date values to Timestamp values with reasonably semantics: if you use the Date.equals(Timestamp) interface, you just ignore the fractional part, which is presumably what you meant, since you used an interface that has nothing to do with fractional parts; if you use the Timestamp.equals(Date) interface, it can only be true if the Timestamp has zero fractional milliseconds.

Alas, this is not the case, which makes the Timestamp class far less useful
import java.util.Date;
import java.sql.Timestamp;

public class Foo
{
public static void main(final String[] args)
{
System.out.println("D == T: "
+ (new Date(1000)).equals(new Timestamp(1000)));
System.out.println("T == D: "
+ (new Timestamp(1000)).equals(new Date(1000)));
}
}

$ java Foo
D == T: true
T == D: false

And, in case you're not yet convinced that this is complete madness, check out the compareTo(Date) method, which states:
Compares this Timestamp object to the given Date, which must be a Timestamp object. If the argument is not a Timestamp object, this method throws a ClassCastException object. (Timestamp objects are comparable only to other Timestamp objects.)

So if you pass a Date to the compareTo(Date) method, it throws. Madness.