为什么compareTo()方法会在排序时导致合同违规?

时间:2017-03-04 22:24:36

标签: java

我有一个文件名列表,想按以下顺序进行比较:

  • 以" .rar"结尾的所有名称应该来 文件之前使用" .r01"," .r02",...
  • 以" .par2"结尾的所有名称应该 带有任何其他后缀的文件后

所以我在我的一个Java类中使用以下compareTo方法:

public class DownloadFile implements Comparable<DownloadFile>
{
    // custom code ...

    @Override
    public int compareTo(DownloadFile other)
    {
        if(other == null)
            throw new NullPointerException("Object other must not be null");

        // special cases -- .rar vs .par2 etc.
        String thisStr = filename.toLowerCase();
        String oStr = other.getFilename().toLowerCase();
        if(thisStr.endsWith(".rar") && oStr.matches(".*\\.r[0-9]{2,}$"))
            return -1;
        if(thisStr.matches(".*\\.r[0-9]{2,}$") && oStr.endsWith(".rar"))
            return 1;
        if(!thisStr.endsWith(".par2") && oStr.endsWith(".par2"))
            return -1;
        if(thisStr.endsWith(".par2") && !oStr.endsWith(".par2"))
            return 1;

        // normal comparison based on filename strings
        return thisStr.compareTo(oStr);
    }
}

然而,在一些数据上,这导致了以下的执行:

Exception in thread "Thread-12" java.lang.IllegalArgumentException: Comparison method violates its general contract!

我试着理解我在这里缺少的东西,但我无法找到问题 你能否发现我违反合同的地方?

PS:如果我注释掉了后两个if,那么仍会抛出异常。所以问题在于前两个if

1 个答案:

答案 0 :(得分:5)

这不是传递性的 元素的线性排序是不可能的。

通过反例证明。

假设您有3个DownloadFilecba),其名称为小写:

c.par2
b.notpar2
a.par2

为了简化,我将使用<进行线性排序,并使用小写的名称。

c.par2 < b.notpar2b.notpar2 < a.par2,但c.par2 < a.par2并非如此 这种关系不是transitive

在逻辑上...就像:

cRbbRa,但cRa并非如此。

您所要做的就是回答如何线性订购文件...
我会选择这样的东西:

if(aMethodOnThis < aMethodOnOther) {
    return -1; //or 1
}
if(aCompletelyDifferentCriterium) {
    //...
}
return 0; //or return thisFileName.compareTo(otherFileName);

最后的return 0非常重要,因为它返回了无法区分的文件。

在那种情况下:

public class DownloadFile implements Comparable<DownloadFile>{

    String filename;

    DownloadFile(String filename) {
        this.filename = filename;
    }

    public String getFilename() {
        return this.filename;
    }

    @Override
    public String toString() {
        return this.getFilename();
    }

    @Override
    public int compareTo(DownloadFile downloadFile) {
        String thisStr = this.filename.toLowerCase();
        String oStr = downloadFile.getFilename().toLowerCase();
        if(thisStr.endsWith(".rar")) {
            if(!oStr.endsWith(".rar"))
                return -1;
        }
        if(oStr.endsWith(".rar")) {
            if(!thisStr.endsWith(".rar"))
                return 1;
        }
        if(thisStr.matches(".*\\.r[0-9]{2,}$")) {
            if(!oStr.matches(".*\\.r[0-9]{2,}$"))
                return -1;
        }
        if(oStr.matches(".*\\.r[0-9]{2,}$")) {
            if(!thisStr.matches(".*\\.r[0-9]{2,}$"))
                return 1;
        }
        if(thisStr.endsWith(".par2")) {
            if(!oStr.endsWith(".par2"))
                return -1;
        }
        if(oStr.endsWith(".par2")) {
            if(!thisStr.endsWith(".par2"))
                return 1;
        }
        return thisStr.compareTo(oStr);
    }

    public static void main(String[] args) {
        List<DownloadFile> fileList = new ArrayList<>();
        fileList.add(new DownloadFile("a.rar"));
        fileList.add(new DownloadFile("b.rar"));
        fileList.add(new DownloadFile("a.r01"));
        fileList.add(new DownloadFile("b.r01"));
        fileList.add(new DownloadFile("a.r10"));
        fileList.add(new DownloadFile("b.r10"));
        fileList.add(new DownloadFile("a.par2"));
        fileList.add(new DownloadFile("b.par2"));
        fileList.add(new DownloadFile("a.other"));
        fileList.add(new DownloadFile("b.other"));
        Collections.shuffle(fileList);
        Collections.sort(fileList);
        System.out.println(fileList);
    }
}

从Java 8缩短Predicate<String>会派上用场;)

@Override
public int compareTo(DownloadFile downloadFile) {
    String thisStr = this.filename.toLowerCase();
    String oStr = downloadFile.getFilename().toLowerCase();
    List<Predicate<String>> conditionList = new ArrayList<>();
    conditionList.add(s -> s.endsWith(".rar"));
    conditionList.add(s -> s.matches(".*\\.r[0-9]{2,}$"));
    conditionList.add(s -> s.endsWith(".par2"));
    for(Predicate<String> condition : conditionList) {
        int orderForCondition =
                conditionHelper(thisStr, oStr, condition);
        if(orderForCondition != 0)
            return orderForCondition;
    }
    return thisStr.compareTo(oStr);
}

private int conditionHelper(String firstStr, String secondStr,
                            Predicate<String> condition) {
    if(condition.test(firstStr))
        if(!condition.test(secondStr))
            return -1;
    if(condition.test(secondStr))
        if(!condition.test(firstStr))
            return 1;
    return 0;
}