Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Behaviour of compareTo() and related #132
Following the calendar system hierarchy changes, the compareTo() method definitions have changed.
A 310 goal has been that the value type classes should have equals() and compareTo() work together such that compareTo() only returns zero when equals() is true. This fits with a potential future with operator overloading. I also note that Joda-Time had a performance hotspot around the implementation of compareTo)
The equals() method definition is effectively fixed. It has to check the effective state of the date (LocalDate or FooDate). That means it checks both the date on the time-line and the chronology. Checking of the object type is optional, but it is simpler to do so, thus LocalDate is only equal to LocalDate.
The equals method implies that compareTo is an implementation of
Observation 1: Semantically, the most desired comparison for
Option 1: Is it necessary for
Option 2: Use
I'm working hard to ensure that the key classes can have operator overloading in the future if JVM value types are introduced in a backwards compatible way. That requires the restriction in para 2.
Option 3: Use
Para 2 is a tighter constraint than necessary and being forced to meet it contradicts the general contract of Comparable to support the natural ordering of the type. Any future language change will need to take into account the allowable difference between equals() and Comparable == 0. The contract for Comparable is only that equals()== true then Comparable == 0 but not the other way around.
Is the argument about a specific
Option 3 would effectively disable the collection classes for sorted Dates (except for LocalDate) since it would not be possible to do the necessary comparisons to do the sorting. I don't know of a declaration syntax to prevent
Para 2 is not a nice to have its essential to real developers and causes all sorts of problems when broken. I've had to hack around the problem in BigDecimal on a number of occasions (bugs), and it is the cause of various odd pieces of behaviour in the collections lilbrary (which cause more bugs).
"It is strongly recommended (though not required) that natural orderings be consistent with equals. This is so because sorted sets (and sorted maps) without explicit comparators behave "strangely" when they are used with elements (or keys) whose natural ordering is inconsistent with equals. In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of the equals method"
As noted in Effective Java, inheritance problems apply to both equals and compareTo, and the generally accepted solution is composition, not inheritance - one reason why I argued against inheritance. Seeing as we now have a form of inheritance, we have to make it work as best we can.
(A) the Chrono level does not implement Comparable - only LocalDate/MinguoDate/CopticDate do
Option (C) cause problems if MinguoDate or similar are made public in the future (as then equals/compareTo would clash). This doesn't really work.
Option (B) requires LocalDate to handle arbitrary ChronoLocalDate implementations in equals() which is a performance drain in a critical method. The same applies to compareTo(). (While your special override suggestion would work in some cases, generally comparable is used via the interface).
It also causes runtime errors, as most chrono-level developers will be using
Option (A) causes no new issues. Developers at the chrono-level must actively choose to use a comparator, something which makes perfect sense, as they are having to choose to throw away some information held in the objects (the chronology) while doing the comparison. The comparator can be on ChronoLocalDate, similar to:
public static final Comparator DATE_COMPARATOR = EPOCH_DAYS;
Finally, in option A, the immutable date classes like MinguoDate implement Comparable even though that will not be publicly visible in the type system. Some classes, like TreeMap work with types without requiring the visible Comparable nature - sadly Collections.sort does not.
Personally I don't think there is a choice here...
I don't agree that the "consistent with equals" requirement must be met by DateTime. The Comparable<Chronology<?>> construct and implementation has a clean definition and does not lead to runtime exceptions due to a constraint not enforceable at compile time. The potential for explicit comparators can be used regardless of the default behavior to be more specific about the comparison but is less usable because it forces extra coding even for the expected case.
Can you be more specific about the kinds of errors you had to hack around with BigDecimal?
This needs more time to evaluate.
The problems with
Similar issues to this case occur with
The errors you see are where
There is an option (D) - implement
Option A does not provide methods to compare dates as equal/before/after on ChronoLocalDate. Those seem to be essential for working with dates. The need for the comparisons is independent of whether they implement
Option D seems attractive but including the chronology in the timeline would be surprising to many and a separate method to compare only using the timeline will be needed anyway.
Developers will understand that comparing dates with equals and compareTo is constrained to operate only on a single type whether it is LocalDate or ChronoLocalDate and that comparing across chronologies needs specific handling. For Collections and sorting, it can be via a Comparator that uses EPOCH_DAY and for direct comparisons equalsDate or similar.
The methods for comparing dates for equals, is-before, is-after, and compare are needed to support the multi-calendar API on ChronoLocalDate. These methods are needed regardless of whether the Comparable interface is present on ChronoLocalDate and are needed and would be inherited by LocalDate.
For Collections to be sortable, the concrete types need to implement Comparable but that's not an issue. BTW, Collections.sort(List, Comparator) can be used on types that do not implement Comparable. Arrays.sort uses Comparator also. Implementing Comparable is a plus.
Multiple concrete implementations of a particular chronology are not necessary and it will simplify the implementation to specify that each concrete type does not need to interoperate with another implementation of the Chrono* type. For example, the only implementation of ChronoLocalDate is LocalDate. That will also discourage anyone from trying to create competing implementations. This allows each date type to have a concrete implementation that operates only with itself. ClassCastExceptions would be expected for equals, compareTo, isBefore, and isAfter. The generics should prevent the common improper uses at compile time and the vm should optimize the casts.
If needed, overloading of equals and compareTo in LocalDate could avoid extra type checking necessary for the most generic ChronoLocalDate and Comparable interfaces.
Option B with some refinements balances the tension between API usability and performance.
Option A can provide the isAfter/isBefore/equalDate methods, just not compareTo. Those methods have no requirement to be limited by "consistent with equals" behaviour.
Option D is essentially the same as what we do elsewhere, such as the Offset* classes. There, comparable is defined in a manor consistent with equals, and isAfter/isBefore are time-line based. (This option provides a stable total ordering for the objects. A list of dates will be in time-line order, its just that two dates in two different chronologies will also have an order, rather than being ==0).
Option B will fail on collections of mixed date types. I think that the globalization team should comment on this, because my expectation based on what they've said so far is that they would always use
I believe B will cause more bugs than D. A is more immediately inconvenient but results in fewest bugs. I'm arguing strongly for D over B.
A date comparator constant makes sense for any option where the base comparison is not time-line based. (An instant comparator should be added to Offset* classes)
I would be willing for there to be a broader discussion in OpenJdk/Oracle on this matter. In other words, if Oracle could change
ChronoLocalDate does not imply mixing ChronoXX types, Javac creates distinct bindings of for the generics that are traced.
Having compareTo not being consistent with equals is not an option. Option D seems the best so far. However, unless I missed something, there is nothing preventing two chronologies from having the same ID, so Chronology.identityHashCode needs to be used as the last resort in comparison if two different instances have the same name.
The use cases for homogenous date-time types far outweigh those for mixed types and should be well supported by the common APIs in the JDK. The mixed date-times cases are in the minority and their design should not disable the most common use cases.
The compiler will flag most cases of mixing generics for both B and D; with both it is possible to compile code that will fail at runtime due to type mismatches.
B supports the common case of homogenous dates better than D using the familiar APIs and types including LocalDate. A few APIs operate on Comparable, but most are either agnostic at compile time or use Comparator which may still fail at runtime.
To support the homogeneous case the concrete classes should implement Comparable such as LocalDate and JapaneseDate. As the functions for sorting are implemented, there is no difference between between B and D with respect to being able to create and use mixed collections. The compareTo methods throw class cast exceptions with mixed contents and work just fine with homogenous types.
For the homengeous types, equals and Comparable.compareTo provide the best ease of use the compiler is verifying types are consistently used.
Option D to seems to conclude that the Comparable mechanism is unusable for Threeten and all comparisons must be done with Comparators or new methods. I think it is that bad but with to the 'consistent with equals' argument it is necessary to support two parallel comparison mechanisms.
I'll ask about the case with BigDecimal.
Really? Oracle's I18N requirements indicated that they would always refer to dates in an abstract way rather than a specific one, so this means that collections are of
With option B, sorting a
isAfter/isBefore exist to support if statements in business logic. They are needed on
It simply observes that just as equals doesn't play well with inheritance, nor does compareTo. However, compareTo in our case also doesn't play that well with composition.
@michael, you are right about clashing chronology IDs, although I'd be willing to just put that one in the Javadoc.
Easy enough, declare the Chronology parameter explicitly in the method declaration and update the references to be
The RAW types warnings are eliminated and it is better usage. Relying on <?> is bad practice and is usually not necessary.
Try rewriting the method in the way that most developers will using ? and you'll see that
All this can be solved by choosing option D, which works the same as B for homogenous, but also works for heterogenous.
It is surprising for Option D to include chronology on the timeline, as Roger pointed out.
On the other hand, as Stephen pointed out, we anticipate mixed chrono sorting scenarios (e.g. some non-ISO enabled reservation system) so ability to sort
I think it is fine for equals() to return false if the chronology is different, but compareTo() should return zero if the date is the same regardless of the chronology. This is because a chronology is merely a system to identify the dates, that are essentially totally universal; the earth spins around one turn a day. All dates in one chronology have a corresponding day in the others as far as they are in the defined range of dates for the chronology.
In summary, may I ask the possibility to modify Option D to remove chronology from the timeline?
Option F: implement
Option F would make compareTo inconsistent with equals which isn't acceptable as it has lots of unexpected/negative effects.
On my blog's comments, it seems like people ultimately gravitated towards not implementing Comparable (I explained the similar case on OffsetDateTime as it was easier to grasp).
I think option D with a comparator for date-order is the best option where CLD implements Comparable. Option A (don't implement Comparable) also works for me - it just forces people to use a comparator.