Saturday afternoon. Perfect timing for an exhaustive debug session.
So here we are, debugging a relatively straight-forward algorithm that handles objects based on their Date field. Supposedly, a piece of cake.
A few hours later (and 3 mins prior to solving the issue), we notice that a collection of Date objects sometimes contains objects of type TimeStamp.
A quick glimpse on the TimeStamp class javadoc reveals the following:
TimeStamp is a composite of a java.util.Date and a separate nanoseconds value
and
The Timestamp.equals(Object) method never returns true when passed a value of type java.util.Date because the nanos component of a date is unknown.
To put the same statements into something more comprehensible:
Date d = new Date(); Date ts = new Timestamp(d.getTime()); System.out.println(ts.equals(d)); //returns false System.out.println(d.equals(ts)); //returns true System.out.println(ts.compareTo(d)); //return 0 System.out.println(d.compareTo(ts)); //returns 1
Now, it’s not very polite to have two references of type Date, with ‘o1.equals(o2) = true’ and ‘o2.equals(o1) = false’ :) If you have an application that fetches data from the underlying database via ORM (in this scenario we use Hibernate) and from the GUI, chances are you will have this situation.
But the biggest problem has yet to come!
By now, we all learned that TimeStamp extends Date and adds the nanosecond ’support’. OK, let’s accept that. So when implementing the compareTo method, they obviously took care to check if it the argument they’re comparing to was a TimeStamp or a Date. If it was a Date, they created a temporary TimeStamp object from its milliseconds-from-1-1-1970 value (getTime() method), and the output was correct (in the example above, it returns 0). But what about the last example? ‘d.compareTo(ts) = 1′ ?! How can that be? If the Date object has no perception of nanos and the TimeStamp object is created from its getTime() method, how can the Date be *after* the TimeStamp?
By bad design decisions, i dare say :)
In the last paragraph, when i told you about the TimeStamp’s compareTo(Date d) method, I left something out intentionally. Now i can drive the point home :) Here’s the original method implementation (taken from the java.sql.TimeStamp source code):
public int compareTo(java.util.Date o) {
if(o instanceof Timestamp) {
// When Timestamp instance compare it with a Timestamp
// Hence it is basically calling this.compareTo((Timestamp))o);
// Note typecasting is safe because o is instance of Timestamp
return compareTo((Timestamp)o);
} else {
// When Date doing a o.compareTo(this)
// will give wrong results.
Timestamp ts = new Timestamp(o.getTime());
return this.compareTo(ts);
}
}
Notice the comments in the ‘else’ block? Nice!
After some more digging through the TimeStamp and Date implementation, we found the epicenter of this stupidity :) Take a look at the TimeStamp constructor:
public Timestamp(long time) {
super((time/1000)*1000);
nanos = (int)((time%1000) * 1000000);
if (nanos < 0) {
nanos = 1000000000 + nanos;
super.setTime(((time/1000)-1)*1000);
}
}
See the first line? It removes the millisecond part and calls the Date constructor. Everything smaller than a second is kept in the ‘nanos’ field. Very clever. Although it’s not a photon, TimeStamp is of dual nature :)
When calling the compareTo method on a Date object it compares the ‘fastTime’ fields. Unfortunately, TimeStamp changed the original ‘fastTime’ field by subtracting everything that’s smaller than a second and putting that data in the separate field called ‘nanos’. Meanwhile, calling the equals() method will compare the same object by calling their getTime() method which will produce the correct result since the TimeStamp implementation takes care of adding back the ‘nanos’ part to the ‘fastTime’ field.
Sun Microsystems, you’re doing it wrong.
Conclusion: when using TimeStamp and Date objects interchangeably, the preferred way to go is force casting to one of them…
p.s. credits for this post go to !alk and fressner, we all invested a few hours in this post :)
[Update 16/10] Seems we were the only ones not using the Joda Time library