Comparator general contract violation - java

I have read all thread about transitive comparator, I don't get why this comparator function is violating a rule. If someone can clean my eyes, it is quite simple I think but I cannot get it
Stack is:
java.util.TimSort.mergeLo(TimSort.java:747)
java.util.TimSort.mergeAt(TimSort.java:483)
java.util.TimSort.mergeCollapse(TimSort.java:410)
my object (simplified)
public class SleepDetails {
private DateTime time;
private SleepEnum type;
[...]
}
public enum SleepEnum {
DEEP(0), LIGHT(1), AWAKE(2), BEGIN(16), END(17);
[...]
}
The comparator a static into a class
Comparator<SleepDetails> comparator = new Comparator<SleepDetails>(){
public int compare(SleepDetails arg0, SleepDetails arg1) {
int res = arg0.getTime().compareTo(arg1.getTime());
if (res != 0)
return res;
if (arg0.getType() == arg1.getType())
return 0;
switch(arg0.getType()) {
case BEGIN:
return -1;
case END:
return 1;
default:
return 0;
}
}
};
Mainly I want to sort the events by date, in case of two events with the same datetime put begin event first and end event as last.
I don't have the collection triggering the bug

If you compare two SleepDetails instances having the same getTime(), and one of them has getType() BEGIN and the other AWAKE.
compare (one, two)
would give -1
while
compare (two, one)
would give 0
This violates the contract :
The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x)) for all x and y.
You must also check arg1.getType() in your compare method (whenever arg0.getType() is neither BEGIN nor END).
public int compare(SleepDetails arg0, SleepDetails arg1) {
int res = arg0.getTime().compareTo(arg1.getTime());
if (res != 0)
return res;
if (arg0.getType() == arg1.getType())
return 0;
switch(arg0.getType()) {
case BEGIN:
return -1;
case END:
return 1;
default:
switch(arg1.getType()) {
case BEGIN:
return 1;
case END:
return -1;
default:
return 0;
}
}
}

The problem is that your code does not distinguish enum values for the type other than BEGIN and END. In particular, it returns zero when the first type is neither BEGIN nor END, regardless of the second type.
However, this behavior is not symmetrical: if you swap the two items in a pair BEGIN and LIGHT, you would get -1 and 0, breaking the symmetry.
You can treat all types other than BEGIN and END as equal to each other, but then you need to use both sides when deciding the equality.

Related

Collections.sort() Comparison method violates its general contract in Java [duplicate]

This question already has answers here:
"Comparison method violates its general contract!"
(13 answers)
Closed 4 years ago.
I know that this kind of question has been asked millions of times if not billions, however I couldn't find my answer yet :)
This compare() method doesn't have Long, Double, Float, ..., it only has Date, boolean, and Null checker, however it shows me that contract violation error, can any one help plz?
Collections.sort(users, new Comparator<MiniUser>() {
#Override
public int compare(MiniUser u1, MiniUser u2) {
boolean resComing = checkMatchConditions(u1,user);
boolean resExists = checkMatchConditions(u2,user);
if(Boolean.valueOf(resComing) && Boolean.valueOf(resExists)) {
if(u1.getLastMatchDate() == null){
return -1;
}else if(u2.getLastMatchDate() ==null ){
return 1;
}else if (u1.getLastMatchDate().toInstant().isBefore(u2.getLastMatchDate().toInstant())){
return -1;
}else {
return 1;
}
}
else if (Boolean.valueOf(resComing)) {
return -1;
}
return 1;
}
});
MiniUser.class
public class MiniUser implements Serializable {
String id;
String name;
Date lastMatchDate;
boolean showCompleteName;
//getters, setters
}
checkMatchConditions return boolean based on some calculations
You should start by reading the JavaDoc of Comparator.compare() to understand what that "contract" is:
The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y,
x)) for all x and y.
In normal terms it says that if "x is greater than y, y must be smaller than x". Sounds obvious, but is not the case in your comparator:
In your case you violate it when two Users have checkMatchConditions false, in which case compare(u1, u2) and compare(u2, u1) both return 1. Hence, there are cases where u1 is greater than u2, and u2 is greater than u1, which is a violation.
Similarely, if both Users have checkMatchConditions true, and their lastMatchDates are both null, they will also violate the contract.
In addition, because you manually try to compare the dates with isBefore, you also return -1 in both cases when two Users have checkMatchConditions true and their lastMatchDates are both equal.
In order to fix this, you should first add a natural language description of how you want the Users to be ordered. Then you can work out the comparator logic.
The error has nothing to do with the Boolean.valueOf() by the way.
Now that you explained how you want to order, have a look at this comparator:
public int compare(MiniUser u1, MiniUser u2)
{
// order by match
boolean u1Matches = checkMatchConditions(u1, user);
boolean u2Matches = checkMatchConditions(u2, user);
if (u1Matches != u2Matches)
{
// put matching ones first
return u1Matches ? -1 : 1;
}
else if (u1Matches)
{
// order by dates
boolean u1HasDate = u1.getLastMatchDate() != null;
boolean u2HasDate = u2.getLastMatchDate() != null;
if (u1HasDate != u2HasDate)
{
// put the ones without date first
return u1HasDate ? 1 : -1;
}
else if (u1HasDate)
{
// order chronologically
return u1.getLastMatchDate().compareTo(u2.getLastMatchDate());
}
else
{
// no dates, no order possible
return 0;
}
}
else
{
// both don't match, no order possible
return 0;
}
}
If I understood your requirements correctly, this should impose a consistent order to your elements. Note how I use Date's compareTo for the date ordering instead of doing it myself, and how I return 0 in case they are "equal" in regards to the order instead of "randomly" returning 1 or -1.
You need to find where sgn(compare(x, y)) == -sgn(compare(y, x)) doesn't hold. I suggest you use brute force to find examples.
Comparator<MiniUser> comp = ...
for (MiniUser a : users) {
for (MiniUser b: users) {
if (a == b) continue;
if (comp.compare(a, b) != -comp.compare(b, a)) {
// print an error message with these two.
}
}
}

Comparing Using 2 Properties in Ascending and Descending Orders

I have an object that has multiple properties, two of which the user can choose to order by, both can be Ascending, Descending, or neither (Normal), and they are independent of each other. So my cases are:
Case 1
propA - Normal
propB - Normal
Case 2
propA - Asc
propB - Normal
Case 3
propA - Desc
propB - Normal
And you get the idea. I'm using a Comparator to do this, and so far I have been able to get it to sort when one or both values are set to Normal. The part that I'm unsure of is what to do when I have chosen to sort by both methods. For example, if I want to order by propA ascending and propB descending, it should look a little like this
propA propB
A Z
A D
B M
B A
R Q
Z Z
Z A
Here is how I'm sorting now
#Override
public int compare(Field lhs, Field rhs) {
switch (growerSort) {
case NORMAL:
switch (fieldSort) {
case NORMAL:
return ((Integer) lhs.getID()).compareTo(rhs.getID());
case ASC:
return lhs.getPropB().toLowerCase().compareTo(rhs.getPropB().toLowerCase());
default:
return rhs.getPropB().toLowerCase().compareTo(lhs.getPropB().toLowerCase());
}
case ASC:
switch (fieldSort) {
case NORMAL:
return lhs.getPropA().toLowerCase().compareTo(rhs.getPropA().toLowerCase());;
case ASC:
return 0; // 0 used as placeholder
default:
return 0; // 0 used as placeholder
}
default:
switch (fieldSort) {
case NORMAL:
return rhs.getPropA().toLowerCase().compareTo(lhs.getPropA().toLowerCase());
case ASC:
return 0; // 0 used as placeholder
default:
return 0; // 0 used as placeholder
}
}
}
How can I sort with two different fields, each with their own order of sorting?
I'm a little bit confused of your Comparator. It's not easy to understand what switch triggers what event.
However I'll describe the standard procedure.
You'll need a priority order over your fields you want to compare. In your example above, I assume it first must be sorted by propA, then by propB.
Then you first sort by propA. If it returns "equals" (zero), then you want to sort by the next field, propB, and so on.
Let me show you an example:
#Override
public int compare(final Field lhs, final Field rhs) {
int firstCompareValue = lhs.getPropA().compareTo(rhs.getPropA());
if (firstCompareValue == 0) {
// lhs and rhs are equals in propA, use propB
int secondCompareValue = lhs.getPropB().compareTo(rhs.getPropB());
return secondCompareValue;
} else {
return firstCompareValue;
}
}
Of course you can also do this iterative if you have multiple fields, as long as you have specified an order (e.g. by using an ordered list over your property fields).
Now you need to add your switches to this showcase :) I'll recommend doing a PropertyComparator for that.
public final class PropertyComparator extends Comparator<Comparable<?>> {
private final boolean mUseDscOrder = false;
public void setUseDscOrder(final boolean useDscOrder) {
mUseDscOrder = useDscOrder;
}
public int compare(final Comparable<?> o1, final Comparable<?> o2) {
if (!mUseDscOrder) {
return o1.compareTo(o2);
} else {
// Reverses the logic, results in DscOrder
return o2.compareTo(o1)
}
}
}
And now use it in the above Comparator.
#Override
public int compare(final Field lhs, final Field rhs, final boolean firstUseDscOrder, final boolean secondUseDcsOrder) {
PropertyComparator firstComparator = new PropertyComparator();
firstComparator.setUseDscOrder(firstUseDscOrder);
int firstCompareValue = firstComparator.compare(lhs.getPropA(), rhs.getPropA());
if (firstCompareValue == 0) {
// lhs and rhs are equals in propA, use propB
PropertyComparator secondComparator = new PropertyComparator();
secondComparator.setUseDscOrder(secondUseDscOrder);
int secondCompareValue = secondComparator.compare(lhs.getPropB(), rhs.getPropB());
return secondCompareValue;
} else {
return firstCompareValue;
}
}
I've not tested it but I think you get the idea :)
Create 1 comparator that sorts on the "first field". If the values of the first field are equal, sort on the "second field".
if(object1.f1.equals(object2.f1)){
object1.f2.compareTo(object2.f2);
} else {
object1.f1.compareTo(object2.f1);
}

Why does my compare method violate its general contract?

public static Comparator<Container> DEPARTURE = new Comparator<Container>() {
#Override
public int compare(Container container1, Container container2) {
if (container1.departure.time.isBefore(container2.departure.time))
return -1;
else if (container1.departure.time.equals(container2.departure.time) &&
container1.departure.maxDuration == container2.departure.maxDuration &&
container1.departure.transportCompany.equals(container2.departure.transportCompany) &&
container1.departure.transportType == container2.departure.transportType)
return 0;
else
return +1;
}
};
the departure variable is just an instance of an object containing the following fields:
public DateTime time;
public int maxDuration;
public TransportType transportType;
public String transportCompany;
P.S. the time object is an instance of DateTime from the Joda-Time library and TransportType is an enumeration containing the constants Train, Seaship, Barge and Truck.
EDIT:
Ok, so, I edited my comparator to the following:
public static Comparator<Container> DEPARTURE = new Comparator<Container>() {
#Override
public int compare(Container container1, Container container2) {
if (container1.departure.time.isBefore(container2.departure.time))
return -1;
else if (container1.departure.time.isBefore(container2.departure.time))
return +1;
else {
if (container1.departure.maxDuration == container2.departure.maxDuration && container1.departure.transportType == container2.departure.transportType && container1.departure.transportCompany.equals(container2.departure.transportCompany))
return 0;
else
return +1;
}
}
};
but this obviously violates the general contract. How do I make it so it sorts by time and then sort those objects that have equivalent times by their other attributes only caring if they're equal or not? Hope this makes sense ...
EDIT: SOLUTION
Thank you all for answering my question! After studying your comments I came up with the following solution that seems to work (not thoroughly tested though):
I actually moved the comparing part to departure his class because I also need to compare by arrival. I decided to simply sort by all attributes (consecutively time, maxDuration, transportCompany and transportType) and the solution I came up with is:
public static Comparator<Container> ARRIVAL = new Comparator<Container>() {
#Override
public int compare(Container container1, Container container2) {
return container1.arrival.compareTo(container2.arrival);
}
};
public static Comparator<Container> DEPARTURE = new Comparator<Container>() {
#Override
public int compare(Container container1, Container container2) {
return container1.departure.compareTo(container2.departure);
}
};
And then the compareTo method:
#Override
public int compareTo(LocationMovement lm) {
if (this.time.isBefore(lm.time))
return -1;
else if (this.time.isAfter(lm.time))
return +1;
else {
int c = this.maxDuration - lm.maxDuration;
if (c != 0) return c;
c = this.transportCompany.compareTo(lm.transportCompany);
if (c != 0) return c;
c = this.transportType.ordinal() - lm.transportType.ordinal();
return c;
}
}
The general contract is that
COMPARATOR.compare(a, b) = - COMPARATOR.compare(b, a)
In your case, the code which returns -1 one way could return 0 the other.
In order to implement compare, all of the things you check must have the concept of being "lesser," "greater," or "equal" to one another, and then you must decide the order in which to check them, returning lesser/greater for the first of the items that isn't equal. That way, you satisfy the contract that compare(a, b) must be the converse of compare(b, a). If all of the parts of what you're comparing don't have the concept of "greater" or "lesser" (for instance, transport type), then either you can't implement compare or you must force an arbitrary (but reliable) greater/lesser interpretation on them.
Here's a conceptual example of doing that. In this case, the order I've chosen (arbitrarily) is: The time, the duration, the company, and the type. But a different order may be more reasonable. This is just an example. Also, you haven't said what the type of transportType is, so I've assumed it has a compareTo method; obviously it may not and you may have to adjust that.
public static Comparator<Container> DEPARTURE = new Comparator<Container>() {
#Override
public int compare(Container container1, Container container2) {
int rv;
// Times
rv = container1.departure.time.compareTo(container2.departure.time);
if (rv == 0) {
// Duration
if (container1.departure.maxDuration < container2.departure.maxDuration) {
rv = -1;
}
else if (container1.departure.maxDuration > container2.departure.maxDuration) {
rv = 1;
}
else {
// Transport company
rv = container1.departure.transportCompany.compareTo(container2.departure.transportCompany);
if (rv == 0) {
// Transport type
rv = container1.departure.transportType.compareTo(container2.departure.transportType);
}
}
}
return rv;
}
};
Note that if two containers c1 and c2 have equal departure.time, but differ in the other attributes, then both compare(c1, c2) and compare(c2, c1) will return +1, i.e., c1>c2 and c2>c1.
Instead, you should either drop the other fields entirely, or compare them separately in nested or sequential if-elses in case the departure time is equal.
Take a look at this answer to a related question for a clean way to compare objects by multiple attributes.

what would be a good hash function for an integer tuple?

I have this class...
public class StartStopTouple {
public int iStart;
public int iStop;
public int iHashCode;
public StartStopTouple(String start, String stop) {
this.iStart = Integer.parseInt(start);
this.iStop = Integer.parseInt(stop);
}
#Override
public boolean equals(Object theObject) {
// check if 'theObject' is null
if (theObject == null) {
return false;
}
// check if 'theObject' is a reference to 'this' StartStopTouple... essentially they are the same Object
if (this == theObject) {
return true;
}
// check if 'theObject' is of the correct type as 'this' StartStopTouple
if (!(theObject instanceof StartStopTouple)) {
return false;
}
// cast 'theObject' to the correct type: StartStopTouple
StartStopTouple theSST = (StartStopTouple) theObject;
// check if the (start,stop) pairs match, then the 'theObject' is equal to 'this' Object
if (this.iStart == theSST.iStart && this.iStop == theSST.iStop) {
return true;
} else {
return false;
}
} // equal() end
#Override
public int hashCode() {
return iHashCode;
}
}
... and I define equality between such Objects only if iStart and iStop in one Object are equal to iStart and iStop in the other Object.
So since I've overridden equals(), I need to override hashCode() but I'm not sure how to define a good hash function for this class. What would be a good way to create a hash code for this class using iStart and iStop?
I'd be tempted to use this, particularly since you're going to memoize it:
Long.valueOf((((long) iStart) << 32) | istop)).hashcode();
From Bloch's "Effective Java":
int iHashCode = 17;
iHashCode = 31 * iHashCode + iStart;
iHashCode = 31 * iHashCode + iStop;
Note: 31 is chosen because the multiplication by 31 can be optimized by the VM as bit operations. (But performance is not useful in your case since as mentioned by #Ted Hopp you are only computing the value once.)
Note: it does not matter if iHashCode rolls over past the largest int.
the simplest might be best
iHashCode = iStart^iStop;
the XOR of the two values
note this will give equal hashcodes when start and stop are swapped
as another possibility you can do
iHashCode = ((iStart<<16)|(iStart>>>16))^iStop;
this first barrel shifts start by 16 and then xors stop with it so the least significant bits are put apart in the xor (if start is never larger than 65k (of more accurately 2^16) you can omit the (iStart>>>16) part)

Treeset.contains() problem

So I've been struggling with a problem for a while now, figured I might as well ask for help here.
I'm adding Ticket objects to a TreeSet, Ticket implements Comparable and has overridden equals(), hashCode() and CompareTo() methods. I need to check if an object is already in the TreeSet using contains(). Now after adding 2 elements to the set it all checks out fine, yet after adding a third it gets messed up.
running this little piece of code after adding a third element to the TreeSet, Ticket temp2 is the object I'm checking for(verkoopLijst).
Ticket temp2 = new Ticket(boeking, TicketType.STANDAARD, 1,1);
System.out.println(verkoop.getVerkoopLijst().first().hashCode());
System.out.println(temp2.hashCode());
System.out.println(verkoop.getVerkoopLijst().first().equals(temp2));
System.out.println(verkoop.getVerkoopLijst().first().compareTo(temp2));
System.out.println(verkoop.getVerkoopLijst().contains(temp2));
returns this:
22106622
22106622
true
0
false
Now my question would be how this is even possible?
Edit:
public class Ticket implements Comparable{
private int rijNr, stoelNr;
private TicketType ticketType;
private Boeking boeking;
public Ticket(Boeking boeking, TicketType ticketType, int rijNr, int stoelNr){
//setters
}
#Override
public int hashCode(){
return boeking.getBoekingDatum().hashCode();
}
#Override
#SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
public boolean equals(Object o){
Ticket t = (Ticket) o;
if(this.boeking.equals(t.getBoeking())
&&
this.rijNr == t.getRijNr() && this.stoelNr == t.getStoelNr()
&&
this.ticketType.equals(t.getTicketType()))
{
return true;
}
else return false;
}
/*I adjusted compareTo this way because I need to make sure there are no duplicate Tickets in my treeset. Treeset seems to call CompareTo() to check for equality before adding an object to the set, instead of equals().
*/
#Override
public int compareTo(Object o) {
int output = 0;
if (boeking.compareTo(((Ticket) o).getBoeking())==0)
{
if(this.equals(o))
{
return output;
}
else return 1;
}
else output = boeking.compareTo(((Ticket) o).getBoeking());
return output;
}
//Getters & Setters
On compareTo contract
The problem is in your compareTo. Here's an excerpt from the documentation:
Implementor must ensure sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) for all x and y.
Your original code is reproduced here for reference:
// original compareTo implementation with bug marked
#Override
public int compareTo(Object o) {
int output = 0;
if (boeking.compareTo(((Ticket) o).getBoeking())==0)
{
if(this.equals(o))
{
return output;
}
else return 1; // BUG!!!! See explanation below!
}
else output = boeking.compareTo(((Ticket) o).getBoeking());
return output;
}
Why is the return 1; a bug? Consider the following scenario:
Given Ticket t1, t2
Given t1.boeking.compareTo(t2.boeking) == 0
Given t1.equals(t2) return false
Now we have both of the following:
t1.compareTo(t2) returns 1
t2.compareTo(t1) returns 1
That last consequence is a violation of the compareTo contract.
Fixing the problem
First and foremost, you should have taken advantage of the fact that Comparable<T> is a parameterizable generic type. That is, instead of:
// original declaration; uses raw type!
public class Ticket implements Comparable
it'd be much more appropriate to instead declare something like this:
// improved declaration! uses parameterized Comparable<T>
public class Ticket implements Comparable<Ticket>
Now we can write our compareTo(Ticket) (no longer compareTo(Object)). There are many ways to rewrite this, but here's a rather simplistic one that works:
#Override public int compareTo(Ticket t) {
int v;
v = this.boeking.compareTo(t.boeking);
if (v != 0) return v;
v = compareInt(this.rijNr, t.rijNr);
if (v != 0) return v;
v = compareInt(this.stoelNr, t.stoelNr);
if (v != 0) return v;
v = compareInt(this.ticketType, t.ticketType);
if (v != 0) return v;
return 0;
}
private static int compareInt(int i1, int i2) {
if (i1 < i2) {
return -1;
} else if (i1 > i2) {
return +1;
} else {
return 0;
}
}
Now we can also define equals(Object) in terms of compareTo(Ticket) instead of the other way around:
#Override public boolean equals(Object o) {
return (o instanceof Ticket) && (this.compareTo((Ticket) o) == 0);
}
Note the structure of the compareTo: it has multiple return statements, but in fact, the flow of logic is quite readable. Note also how the priority of the sorting criteria is explicit, and easily reorderable should you have different priorities in mind.
Related questions
What is a raw type and why shouldn't we use it?
How to sort an array or ArrayList ASC first by x and then by y?
Should a function have only one return statement?
This could happen if your compareTo method isn't consistent. I.e. if a.compareTo(b) > 0, then b.compareTo(a) must be < 0. And if a.compareTo(b) > 0 and b.compareTo(c) > 0, then a.compareTo(c) must be > 0. If those aren't true, TreeSet can get all confused.
Firstly, if you are using a TreeSet, the actual behavior of your hashCode methods won't affect the results. TreeSet does not rely on hashing.
Really we need to see more code; e.g. the actual implementations of the equals and compareTo methods, and the code that instantiates the TreeSet.
However, if I was to guess, it would be that you have overloaded the equals method by declaring it with the signature boolean equals(Ticket other). That would lead to the behavior that you are seeing. To get the required behavior, you must override the method; e.g.
#Override
public boolean equals(Object other) { ...
(It is a good idea to put in the #Override annotation to make it clear that the method overrides a method in the superclass, or implements a method in an interface. If your method isn't actually an override, then you'll get a compilation error ... which would be a good thing.)
EDIT
Based on the code that you have added to the question, the problem is not overload vs override. (As I said, I was only guessing ...)
It is most likely that the compareTo and equals are incorrect. It is still not entirely clear exactly where the bug is because the semantics of both methods depends on the compareTo and equals methods of the Boeking class.
The first if statement of the Ticket.compareTo looks highly suspicious. It looks like the return 1; could cause t1.compareTo(t2) and t2.compareTo(t1) to both return 1 for some tickets t1 and t2 ... and that would definitely be wrong.

Categories

Resources