Java Collections排序:比较方法违反了其一般合同

时间:2015-01-17 21:01:15

标签: java sorting collections comparison

我知道它已经被问到并且回答了数百万次,但我仍然无法弄清楚为什么我在排序时收到了违规行为。这是我的代码:

Collections.sort(sorted, new Comparator<MyObject>() {
    @Override
    public int compare(MyObject m1, MyObject m2) {
        // Actual energy comparison :-
        // THE higher the energy, the earlier in the list
        float delta = m1.getTotalEnergy() - m2.getTotalEnergy();

        if (delta > 0) {
            return 1;
        } else if (delta < 0) {
            return -1;
        } else {
            return 0;
        }
    }
});

我收到此错误

java.lang.IllegalArgumentException: Comparison method violates its general contract!  
        at java.util.TimSort.mergeHi(TimSort.java:895)  
        at java.util.TimSort.mergeAt(TimSort.java:512)  
        at java.util.TimSort.mergeForceCollapse(TimSort.java:453)  
        at java.util.TimSort.sort(TimSort.java:250)  
        at java.util.Arrays.sort(Arrays.java:1512)  
        at java.util.ArrayList.sort(ArrayList.java:1454)  
        at java.util.Collections.sort(Collections.java:175)

有什么想法吗?

5 个答案:

答案 0 :(得分:6)

假设getTotalEnergy()返回float,您可以使用

return new Float(m1.getTotalEnergy()).compareTo(m2.getTotalEnergy());

使用Float.valueOf(float)可能会稍微提高效率,希望这更容易阅读。

Float f1 = Float.valueOf(m1.getTotalEnergy());
Float f2 = Float.valueOf(m2.getTotalEnergy());
return f1.compareTo(f2);

答案 1 :(得分:1)

不参考MyObject。我的猜测是比较器与MyObject.equal不一致。

也就是说,您违反的合同是:

(comparator.compare(mo1, mo2) == 0) == mo1.equals(mo2)

您的比较器会将具有相同浮点值的对象与相等的比较,其中更复杂的比较器会给出排序,而equals方法会说对象不相等。或者你可能有相反的问题 - equals方法说对象是相同的,比较方法说它们是不同的。

以下情况应该有效。

public int compare(MyObject m1, MyObject m2) {
    if (m1 == m2) return 0;
    if (m1 == null) return -1;
    if (m2 == null) return 1;
    if (m1.equals(m2)) return 0;

    int value = Float.compare(m1.getTotalEnergy(), m2.getTotalEnergy());
    if (value != 0) return value;

    // Warning, this line is not fool proof as unequal objects can have identical hash 
    // codes.
    return m1.hashCode() - m2.hashCode();
}

答案 2 :(得分:0)

也许这会改变:

public int compare(Object m1, Object m2) {
    // Actual energy comparison :-
    // THE higher the energy, the earlier in the list
    float delta = ((MyObject)m1).getTotalEnergy() - ((MyObject)m2).getTotalEnergy();
....
}

答案 3 :(得分:0)

以下代码已使用float,NaN和null值进行了测试。所有案件都得到妥善处理。我为hashCode()类创建了equals()MyObject方法。

结果:[null, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, NaN]

SortObjectFloatProperty

package q28004269;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class SortObjectFloatProperty {
    public static void main(String[] args) {
        List<MyObject> sorted;

        sorted = create(1f, 4f, 6f, null, 8f, 5f, Float.NaN, 3f, 2f, 7f, 0f, 9f);

        Collections.sort(sorted, new Comparator<MyObject>() {
            @Override
            public int compare(MyObject m1, MyObject m2) {
                if (m1 == m2) return 0;
                if (m1 == null) return -1;
                if (m2 == null) return 1;
                if (m1.equals(m2)) return 0;
                int value = Float.compare(m1.getTotalEnergy(), m2.getTotalEnergy());
                if (value != 0) return value;
                return m1.hashCode() - m2.hashCode();
            }
        });

        System.out.println(sorted);
    }

    public static MyObject create(float totalEnergy) {
        return new MyObject(totalEnergy);
    }

    public static List<MyObject> create(Object ...values) {
        List<MyObject> objs = new ArrayList<SortObjectFloatProperty.MyObject>();
        for (int i = 0; i < values.length; i++) {
            if (values[i] instanceof Float) {
                objs.add(create((float) values[i]));
            } else {
                objs.add(null);
            }
        }
        return objs;
    }
}

为MyObject

package q28004269;

public static class MyObject {
    private float totalEnergy;

    public float getTotalEnergy() {
        return totalEnergy;
    }

    public void setTotalEnergy(float totalEnergy) {
        this.totalEnergy = totalEnergy;
    }

    public MyObject() {
        this(0.0f);
    }

    public MyObject(float totalEnergy) {
        this.totalEnergy = totalEnergy;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + Float.floatToIntBits(totalEnergy);
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        MyObject other = (MyObject) obj;
        return !(Float.floatToIntBits(totalEnergy) != Float.floatToIntBits(other.totalEnergy));
    }

    @Override
    public String toString() {
        return Float.toString(totalEnergy);
    }
}

答案 4 :(得分:0)

评论中对NaN值进行了一些讨论。这很重要,因为NaN违反了我们对浮点数比较的典型期望。例如,Double.NaN > 0.0Double.NaN < 1.0都是假的!

这可能导致排序在遇到时抛出异常。即使没有抛出异常,也可能导致列表以错误的顺序排序。因此,您的排序比较器必须处理NaN值。幸运的是,图书馆的内置比较(例如Double.compare())可以为您完成此操作。这与Double.compareTo()具有相同的语义,除了盒装Double值不是必需的。有关详细信息,请参阅documentation。简而言之,NaN值被认为大于所有其他值(包括+Inf),负零小于正零。

要在Double.compare() Comparator中使用sort,请执行以下操作:

Collections.sort(data, new Comparator<MyObject>() {
    public int compare(MyObject m1, MyObject m2) {
        return Double.compare(m1.getTotalEnergy(), m2.getTotalEnergy());
    }
});

如果你正在使用Java 8,你可以这样做:

data.sort(Comparator.comparing(MyObject::getTotalEnergy));

要处理空值,请使用nullsFirstnullsLast包装比较器:

data.sort(Comparator.nullsFirst(Comparator.comparing(MyObject::getTotalEnergy)));