I am creating a java.util.concurrent.ConcurrentSkipListSet with a comparator. My comparator compares an Instant field in the object to determine the order. This is the comparator:
public int compare(myObj first, myObj second) {
if (first == null || first.getTime() == null) {
return 1;
}
if (second == null || second.getTime() == null) {
return -1;
}
if ( first.getTime().isBefore(second.getTime()) {
return -1;
}
else {
return 1;
}
}
When I use the above compartor 5/10 times it is stuck in an infinite loop. First is always after second , so the program always reach the else and return 1, however in some runs it just loops, it reaches the else but is just stuck constantly running the comparator... I can see this when I run in debug mode and also when adding extra logging. To clarify the 2 objects are not the same when it is stuck, so the issue isn't trying to add duplicated. When I switch isBefore to isAfter like so, I don't get that behaviour, it runs as expected each time. My question is why would this happen?
public int compare(myObj first, myObj second) {
if (first == null || first.getTime() == null) {
return 1;
}
if (second == null || second.getTime() == null) {
return -1;
}
if ( first.getTime().isAfter(second.getTime()) {
return 1;
}
else {
return -1;
}
}
Adding to the list implementation (boiled down for simplicity)
My concurrent skip set logic is very simple, I create the set with the compartor above, then myObj instant is initiated to now() + a predetermines number of minutes (hard coded so this calculation will not fail) then my obj is added to the concurrentskipset
private ConcurrentSkipListSet<MyObj> skipSetImpl= new ConcurrentSkipListSet<>(new MyObj.MyobjComp());
// added in a method, using Java Instant
foo.setTime( now.plus(60, ChronoUnit.SECONDS) );
this.skipSetImpl.add(foo); // add to concurrentSkipSet
You are not fulfilling the contract of
Comparator.compare(). Whether this is the source of the problem you are asking about, without yourConcurrentSkipSetcode we can’t tell, but it could very well be.If two
MyObjeither are bothnull, one isnulland the other has anullinstant or both have anullinstant, then your comparator cannot decide their order. If your skip set callscompare(a, b),ashould come last, but if next it callscompare(b, a), suddenlybshould come last. Could this cause it to loop? In any case it’s no nice property of a comparator to be inconsistent, and it will surely lead to problems with other classes that use aComparator.The documentation of the
comparemethod says:This is the part you are breaking. Your skip set should be able to rely on you keeping the contract.
You said:
If so, you need to decide one order for any two
MyObj. One solution is to disallow nulls (throw an exception if anulloccurs). Another is to decide that nulls are equal.A terse way of defining a comparator that puts nulls last is through
Comparator.nullsLast()andComparator.comparing()(since Java 8):Comparator.nullsLast()gives you a comparator that sorts nulls last and compares non-null objects using the comparator supplied as argument. I am also using the two-argcomparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator). The first argument,MyObj::getTime, tells it to compare the instants, and the second argument is the comparator used to compare the instants — in this case again putting nulls last. The resulting comparator does fulfil the contract ofComparator.comparing(). Corner case: it will putnullMyObjreferences beforeMyObjobjects having anullinstant, which is different from what your comparator did.When I recommend the Java 8+ way of defining comparators, it’s not so much because it’s brief. It’s more because it is much harder to get wrong, including that it is hard to break the contract of the
comparemethod.Link: Documentation of
Comparator.compare()