Java 比较问题 - 比较方法违反了其通用约定 [重复]

Java Comparison issue - Comparison method violates its general contract [duplicate]

提问人:jos.mathew123 提问时间:11/16/2023 最后编辑:jos.mathew123 更新时间:11/16/2023 访问量:82

问:

我正在尝试对一些数字进行排序。我收到一个“java.lang.IllegalArgumentException:比较方法违反了其一般约定!执行以下代码时异常。

import org.apache.commons.lang3.StringUtils;

public class ComparatorTest {

    public static void main(String[] args) {
        List<String> ll = List.of("1.A", "1.A.1", "10.A", "10.A.1", "10.A.2", "10.A.3", "12.A", "12.A.1", "12.A.2",
                "12.A.4", "12.A.6", "1A.2", "2.A.1", "2.A.1.b", "2.A.1.b.1", "2.A.1.b.2", "2.A.1.b.3", "20.A.1",
                "20.A.1.a", "20.A.1.b", "20.A.1.b.1", "20.A.1.b.2", "3.A.1", "3.A.1.a", "3.A.1.a.1", "3.A.1.a.2",
                "3.A.1.a.3", "3.A.1.a.4", "3.A.1.b", "3.A.10", "6.A.1", "9.A.1");
        
        ArrayList<String> l2 = new ArrayList<>(ll);
        Collections.sort(l2, (obj1, obj2) -> {
            try {
                String[] prodClass1 = obj1.split("\\.");
                String[] prodClass2 = obj2.split("\\.");
                for (int i = 0; (i < prodClass1.length) && (i < prodClass2.length); i++) {
                    if (!prodClass1[i].equals(prodClass2[i])) {
                        if (StringUtils.isNumeric(prodClass1[i]) && StringUtils.isNumeric(prodClass2[i])) {
                            return Integer.valueOf(prodClass1[i]).compareTo(Integer.valueOf(prodClass2[i]));
                        } else {
                            return prodClass1[i].compareToIgnoreCase(prodClass2[i]);
                        }
                    }
                }
                return obj1.compareToIgnoreCase(obj2);
            } catch (Exception e) {
                e.printStackTrace();
                return obj1.compareToIgnoreCase(obj2);
            }
        });
        System.out.println(l2);
    }
    
}

如果我从列表 ll 中删除任何 1 个元素,代码就可以正常工作。

我认为这必须做一些与equals()和compare()方法有关的事情。但是有人能指出这里的问题吗?

如果我在自定义比较器中添加一个额外的条件,它就可以工作。为什么会这样?任何人都可以查明问题所在。

修改后的代码

Collections.sort(l2, (obj1, obj2) -> {
            try {
                String[] prodClass1 = obj1.split("\\.");
                String[] prodClass2 = obj2.split("\\.");
                for (int i = 0; (i < prodClass1.length) && (i < prodClass2.length); i++) {
                    if (!prodClass1[i].equals(prodClass2[i])) {
                        if (StringUtils.isNumeric(prodClass1[i]) && StringUtils.isNumeric(prodClass2[i])) {
                            return Integer.valueOf(prodClass1[i]).compareTo(Integer.valueOf(prodClass2[i]));
                        } else if (StringUtils.isNumeric(prodClass1[i]) || StringUtils.isNumeric(prodClass2[i])) {
                            return StringUtils.isNumeric(prodClass1[i]) ? -1 : 1;
                        } else {
                            return prodClass1[i].compareToIgnoreCase(prodClass2[i]);
                        }
                    }
                }
                return obj1.compareToIgnoreCase(obj2);
            } catch (Exception e) {
                e.printStackTrace();
                return obj1.compareToIgnoreCase(obj2);
            }
        });

以下是我在添加一些日志后执行代码时的输出:

prodClass1: [3, A, 1, a, 1] Comparing with prodClass2: [2, A, 1, b, 3]
prodClass1: [2, A, 1, b, 3] Comparing with prodClass2: [3, A, 1, a]
prodClass1: [2, A, 1, b, 3] Comparing with prodClass2: [3, A, 1]
Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract!
    at java.base/java.util.TimSort.mergeHi(TimSort.java:903)
    at java.base/java.util.TimSort.mergeAt(TimSort.java:520)
    at java.base/java.util.TimSort.mergeForceCollapse(TimSort.java:461)
    at java.base/java.util.TimSort.sort(TimSort.java:254)
    at java.base/java.util.Arrays.sort(Arrays.java:1515)
    at java.base/java.util.ArrayList.sort(ArrayList.java:1750)
    at java.base/java.util.Collections.sort(Collections.java:179)
    at core.ComparatorTest.main(ComparatorTest.java:30)
Java 排序 java-8 比较器

评论

0赞 Jon Skeet 11/16/2023
我认为,如果您更改代码以在每次进行比较时记录两个输入和结果,那么诊断正在发生的事情会容易得多。该日志应该使它更清晰。我怀疑会有一些 x、y、z,其中 x < y、y < z 但 x >z。
0赞 jos.mathew123 11/16/2023
这是我添加调试后得到的: prodClass1: [3, A, 1, a, 1] 与 prodClass2 比较: [2, A, 1, b, 3] prodClass1: [2, A, 1, b, 3] 与 prodClass2: [3, A, 1, a] prodClass1: [2, A, 1, b, 3] 与 prodClass2 比较: [3, A, 1] 线程“main” java.lang.IllegalArgumentException 异常: 比较方法违反了其一般约定!在 java.base/java.util.TimSort.mergeHi(TimSort.java:903) 在 java.base/java.util.TimSort.mergeAt(TimSort.java:520)
0赞 Jon Skeet 11/16/2023
请将其作为列表放在问题中。仅仅内联阅读真的很难。而且你需要包括结果 - 仅仅看到它比较的内容是没有帮助的。
0赞 Andy Turner 11/16/2023
请注意,在循环中,这与非数字大小写 return 不一致。考虑用于第一次检查。!prodClass1[i].equals(prodClass2[i])prodClass1[i].compareToIgnoreCase(prodClass2[i])prodClass1[i].equalsIgnoreCase(prodClass2[i])
0赞 Andy Turner 11/16/2023
此外,如果数组具有不同的长度,则应根据哪个数组更长返回值(例如,,而不是使用字典字符串比较。return Integer.compare(prodClass1.length, prodClass2.length)

答:

6赞 rzwitserloot 11/16/2023 #1

如果你违反了合同,那么可以做两件事之一,而规范不保证任何一种行为sort

  • 排序的输出完全是狼吞虎咽的。
  • 你会得到这个例外。

因此,如果您没有收到该异常,这并不意味着您的代码很好。没有例外并不能证明没有错误。因此,“如果我删除一个元素,这个异常就会消失”是没有意义的。这里的关键问题是 1A 条目。

您可以放大 ."10.A", "1A.2", "2.A.1"

  • 根据您的代码,10.A 小于 1A.2,因为字符串排序用于第一个组件,并且是负数 - 被视为在 ."10".compareTo("1A")"10""1A"
  • 根据您的代码,1A.2 小于 2.A.1,因为字符串排序用于第一个组件,并且是负数 - 被视为在 ."1A".compareTo("2")"1A""2"

到目前为止,很明显,对吧?然而。。

  • 根据您的代码,10.A 小于 2.A.1,因为整数排序用于第一个组件(因为它们都是数字),而 10 在 2 之后

因此,我们有以下情况:

  • A 在 B 之前,B 在 C 之前......
  • 但 A 在 C 之后。

显然,这是不可能的。

比较方法的一般合同包括:

  • a.equals(b)?然后。a.compareTo(b) == 0
  • a.compareTo(a) == 0.
  • a.compareTo(b)需要与 相反。b.compareTo(a)
  • a.compareTo(b)和方向是一样的吗?那么也必须是那个方向(a 在 b 之前,b 在 c 之前?那么 a 也必须在 c 之前 - 'after' 也是如此)。b.compareTo(c)a.compareTo(c)

你违反了最后一个。第二个代码段通过将 视为 之后来修复它。1A10

您的比较代码违反规则的方式还有很多,而且,正如您所发现的,违反规则并不能保证例外。你“呃,回退到字符串排序”的一般方法是一个糟糕的方法。例如,如果一个比另一个“长”,你不应该回退到那个(相反,较长的方式需要总是从较短的方式中赢/输)。

评论

3赞 Holger 11/17/2023
还有第三种可能的结果:sort 偶然为特定输入生成预期的输出。在测试期间发生时,哪个是最糟糕的......