Collections.sort 使用多个字段进行排序

116

我有一个"Report"对象的列表,其中包含三个字段(全部为字符串类型):

ReportKey
StudentNumber
School

我有一个排序代码,像这样-

Collections.sort(reportList, new Comparator<Report>() {

@Override
public int compare(final Report record1, final Report record2) {
      return (record1.getReportKey() + record1.getStudentNumber() + record1.getSchool())                      
        .compareTo(record2.getReportKey() + record2.getStudentNumber() + record2.getSchool());
      }

});

由于某些原因,我没有排序顺序。有人建议在字段之间加入空格,但为什么呢?

你是否看到代码有任何问题?


它们是固定长度的字段吗?如果 record1.getReportKey() 是 "AB" 并且 record1.getStudentNumber() 是 "CD",但是 record2.getReportKey() 是 "ABCD",会发生什么? - mellamokb
固定长度。抱歉,我忘记提到了。 - Milli Szabo
可能是按多个字段比较对象的最佳方法?的重复问题。 - Benny Bottema
15个回答

180

(原文来自基于多个字段对Java对象列表进行排序的方法)

这个代码片段中可以找到原始可用代码。

使用Java 8 lambda表达式(添加于2019年4月10日)

Java 8通过lambda表达式很好地解决了这个问题(尽管Guava和Apache Commons可能仍然提供更多的灵活性):

Collections.sort(reportList, Comparator.comparing(Report::getReportKey)
            .thenComparing(Report::getStudentNumber)
            .thenComparing(Report::getSchool));

感谢@gaoagong的下面的回答
请注意,这里的优点之一是getter被惰性地评估(例如,只有在相关时才评估getSchool())。
混乱而复杂:手动排序
Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        int sizeCmp = p1.size.compareTo(p2.size);  
        if (sizeCmp != 0) {  
            return sizeCmp;  
        }  
        int nrOfToppingsCmp = p1.nrOfToppings.compareTo(p2.nrOfToppings);  
        if (nrOfToppingsCmp != 0) {  
            return nrOfToppingsCmp;  
        }  
        return p1.name.compareTo(p2.name);  
    }  
});  

这需要大量的打字、维护和容易出错。唯一的优点是getter仅在相关时被调用。

反射方式:使用BeanComparator进行排序

ComparatorChain chain = new ComparatorChain(Arrays.asList(
   new BeanComparator("size"), 
   new BeanComparator("nrOfToppings"), 
   new BeanComparator("name")));

Collections.sort(pizzas, chain);  

显然,这种方式更加简洁,但是由于使用字符串而失去了对字段的直接引用(没有类型安全性、自动重构),因此错误更容易发生。如果一个字段被重命名,编译器甚至不会报告问题。此外,由于该解决方案使用反射,所以排序速度要慢得多。
到达目的地的方法:使用Google Guava的ComparisonChain进行排序。
Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return ComparisonChain.start().compare(p1.size, p2.size).compare(p1.nrOfToppings, p2.nrOfToppings).compare(p1.name, p2.name).result();  
        // or in case the fields can be null:  
        /* 
        return ComparisonChain.start() 
           .compare(p1.size, p2.size, Ordering.natural().nullsLast()) 
           .compare(p1.nrOfToppings, p2.nrOfToppings, Ordering.natural().nullsLast()) 
           .compare(p1.name, p2.name, Ordering.natural().nullsLast()) 
           .result(); 
        */  
    }  
});  

这样更好,但对于最常见的用例需要一些样板代码:默认情况下,空值应该被赋予更小的值。对于空字段,您必须向Guava提供额外的指令以告知其在此情况下要进行何种操作。如果您想要执行特定操作,则这是一种灵活的机制,但通常您希望使用默认情况(即1、a、b、z、null)。
正如下面的评论中所指出的那样,每次比较都会立即评估所有这些getter。
使用Apache Commons CompareToBuilder进行排序
Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return new CompareToBuilder().append(p1.size, p2.size).append(p1.nrOfToppings, p2.nrOfToppings).append(p1.name, p2.name).toComparison();  
    }  
});  

像Guava的ComparisonChain一样,这个库类可以轻松地在多个字段上排序,但是还为null值定义了默认行为(即1、a、b、z、null)。然而,除非您提供自己的比较器,否则您不能指定任何其他内容。
再次注意,在下面的评论中指出,对于每个比较,所有这些getter都会立即评估。
因此,最终只取决于风味和灵活性的需要(Guava的ComparisonChain)与简洁的代码(Apache的CompareToBuilder)之间的平衡。
奖励方法:我发现一个很好的解决方案on CodeReview,它按优先级结合了多个比较器,形成MultiComparator。
class MultiComparator<T> implements Comparator<T> {
    private final List<Comparator<T>> comparators;

    public MultiComparator(List<Comparator<? super T>> comparators) {
        this.comparators = comparators;
    }

    public MultiComparator(Comparator<? super T>... comparators) {
        this(Arrays.asList(comparators));
    }

    public int compare(T o1, T o2) {
        for (Comparator<T> c : comparators) {
            int result = c.compare(o1, o2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }

    public static <T> void sort(List<T> list, Comparator<? super T>... comparators) {
        Collections.sort(list, new MultiComparator<T>(comparators));
    }
}

当然,Apache Commons Collections 已经有一个相关的工具:

ComparatorUtils.chainedComparator(comparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));

完美的解决方案,既能保持代码整洁,又能达到预期目的。 - cryptonkid
感谢您的出色回答,Benny。我有一个场景,其中属性不直接在我的对象内。但是有一个嵌套对象。那么在这种情况下该怎么办呢?例如,在这里Collections.sort(reportList,Comparator.comparing(Report :: getReportKey)             .thenComparing(Report :: getStudentNumber)             .thenComparing(Report :: getSchool)); 在Report对象中我有一个student对象,然后在学生对象中我有一个学生编号。在这种情况下,我们如何进行排序?任何帮助将不胜感激。 - Madhu Reddy
@MadhuReddy 在这个例子中,方法引用被用作lambda表达式,但是你也可以提供一个合适的lambda表达式来返回相应的嵌套字段。 - Benny Bottema
Bonus Method中的chainedComparator非常容易集成。 - glade

156

你认为这段代码有什么问题吗?

有。为什么在比较之前要将三个字段相加呢?

我可能会做类似这样的事情:(假设字段的顺序与希望排序的顺序相同)

@Override public int compare(final Report record1, final Report record2) {
    int c;
    c = record1.getReportKey().compareTo(record2.getReportKey());
    if (c == 0)
       c = record1.getStudentNumber().compareTo(record2.getStudentNumber());
    if (c == 0)
       c = record1.getSchool().compareTo(record2.getSchool());
    return c;
}

请详细说明。那我该怎么做呢?谢谢。 - Milli Szabo
嗨,你可以添加更多的 if (c == 0) 吗?我不确定是否正确,但似乎不是,因为如果第一个条件得到满足,就永远不会进入第二个或第三个……等等。 - user3402040
11
我认为你没有理解a.compareTo(b); 惯例是,当Comparable类型的ab相等时,返回0;当a < b时,返回负整数;当a > b时,返回正整数。请注意不要改变原意。 - Jason S
1
它不起作用。我已经尝试过了,但它不起作用。只有一个compareTo可以工作。 - shimatai

45

我会使用GuavaComparisonChain创建一个比较器:

public class ReportComparator implements Comparator<Report> {
  public int compare(Report r1, Report r2) {
    return ComparisonChain.start()
        .compare(r1.getReportKey(), r2.getReportKey())
        .compare(r1.getStudentNumber(), r2.getStudentNumber())
        .compare(r1.getSchool(), r2.getSchool())
        .result();
  }
}

27

这是一个老问题,所以我看不到Java 8的等价物。以下是针对此特定情况的示例。

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

/**
 * Compares multiple parts of the Report object.
 */
public class SimpleJava8ComparatorClass {

    public static void main(String[] args) {
        List<Report> reportList = new ArrayList<>();
        reportList.add(new Report("reportKey2", "studentNumber2", "school1"));
        reportList.add(new Report("reportKey4", "studentNumber4", "school6"));
        reportList.add(new Report("reportKey1", "studentNumber1", "school1"));
        reportList.add(new Report("reportKey3", "studentNumber2", "school4"));
        reportList.add(new Report("reportKey2", "studentNumber2", "school3"));

        System.out.println("pre-sorting");
        System.out.println(reportList);
        System.out.println();

        Collections.sort(reportList, Comparator.comparing(Report::getReportKey)
            .thenComparing(Report::getStudentNumber)
            .thenComparing(Report::getSchool));

        System.out.println("post-sorting");
        System.out.println(reportList);
    }

    private static class Report {

        private String reportKey;
        private String studentNumber;
        private String school;

        public Report(String reportKey, String studentNumber, String school) {
            this.reportKey = reportKey;
            this.studentNumber = studentNumber;
            this.school = school;
        }

        public String getReportKey() {
            return reportKey;
        }

        public void setReportKey(String reportKey) {
            this.reportKey = reportKey;
        }

        public String getStudentNumber() {
            return studentNumber;
        }

        public void setStudentNumber(String studentNumber) {
            this.studentNumber = studentNumber;
        }

        public String getSchool() {
            return school;
        }

        public void setSchool(String school) {
            this.school = school;
        }

        @Override
        public String toString() {
            return "Report{" +
                   "reportKey='" + reportKey + '\'' +
                   ", studentNumber='" + studentNumber + '\'' +
                   ", school='" + school + '\'' +
                   '}';
        }
    }
}

1
比较和thenComparing胜利! - asgs
1
它要求最低的安卓版本为24。 - viper

16
如果您想按报告键、学生编号、学校的顺序进行排序,那么您应该这样做:
public class ReportComparator implements Comparator<Report>
{
    public int compare(Report r1, Report r2)
    {
        int result = r1.getReportKey().compareTo(r2.getReportKey());
        if (result != 0)
        {
            return result;
        }
        result = r1.getStudentNumber().compareTo(r2.getStudentNumber());
        if (result != 0)
        {
            return result;
        }
        return r1.getSchool().compareTo(r2.getSchool());
    }
}

当然,这假定没有任何值为空——如果您需要允许报告、报告关键字、学生编号或学校为空,则变得更加复杂。

虽然您可以使用空格使字符串拼接版本正常工作,但如果您的数据本身包含空格等奇怪情况,它仍将失败。上面的代码是您想要的逻辑代码...首先按报告关键字进行比较,然后仅在报告关键字相同的情况下处理学生编号等。


6
虽然这段代码本身没有问题,我也理解它。但是我更喜欢Jason的实现方式,因为他只有一个返回语句,看起来更易于理解跟随。 - jzd
当我尝试使用您的代码时,它无法使用“compareTo()”方法。您能否帮助我解决这个问题? - viper
@viper:不,因为我正在实现Comparator<T>而不是Comparable<T>。我们不知道你想要实现什么,或者你已经尝试了什么,或者出了什么问题。也许你应该问一个新的问题,在做更多的研究之后(它很可能已经被问过了)。 - Jon Skeet

7

我建议使用Java 8的Lambda方法:

List<Report> reportList = new ArrayList<Report>();
reportList.sort(Comparator.comparing(Report::getRecord1).thenComparing(Report::getRecord2));

有任何 Kotlin 的解决方案吗? - famfamfam

6

Java8中使用多个字段进行排序

package com.java8.chapter1;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import static java.util.Comparator.*;



 public class Example1 {

    public static void main(String[] args) {
        List<Employee> empList = getEmpList();


        // Before Java 8 
        empList.sort(new Comparator<Employee>() {

            @Override
            public int compare(Employee o1, Employee o2) {
                int res = o1.getDesignation().compareTo(o2.getDesignation());
                if (res == 0) {
                    return o1.getSalary() > o2.getSalary() ? 1 : o1.getSalary() < o2.getSalary() ? -1 : 0;
                } else {
                    return res;
                }

            }
        });
        for (Employee emp : empList) {
            System.out.println(emp);
        }
        System.out.println("---------------------------------------------------------------------------");

        // In Java 8

        empList.sort(comparing(Employee::getDesignation).thenComparing(Employee::getSalary));
        empList.stream().forEach(System.out::println);

    }
    private static List<Employee> getEmpList() {
        return Arrays.asList(new Employee("Lakshman A", "Consultent", 450000),
                new Employee("Chaitra S", "Developer", 250000), new Employee("Manoj PVN", "Developer", 250000),
                new Employee("Ramesh R", "Developer", 280000), new Employee("Suresh S", "Developer", 270000),
                new Employee("Jaishree", "Opearations HR", 350000));
    }
}

class Employee {
    private String fullName;
    private String designation;
    private double salary;

    public Employee(String fullName, String designation, double salary) {
        super();
        this.fullName = fullName;
        this.designation = designation;
        this.salary = salary;
    }

    public String getFullName() {
        return fullName;
    }

    public String getDesignation() {
        return designation;
    }

    public double getSalary() {
        return salary;
    }

    @Override
    public String toString() {
        return "Employee [fullName=" + fullName + ", designation=" + designation + ", salary=" + salary + "]";
    }

}

empList.sort(comparing(Employee::getDesignation).thenComparing(Employee::getSalary)); 这段代码帮了我很大的忙。谢谢。 - jarvo69
它要求使用最低的安卓版本为24。 - viper
无法解决方法比较,即使我已经使用了Java 8。您必须使用sort(Comparator.comparing! - Darksymphony

5
如果学生编号是数字,它将不按数字排序,而是按字母数字混合排序。请注意,不要期望


"2" < "11"

这将是:

"11" < "2"

这回答了为什么结果错误的实际问题。 - Florian F

4

使用在JDK1.8中引入的Comparator接口及其方法:comparingthenComparing,或更具体的方法:comparingXXXthenComparingXXX

例如,如果我们想要按照id、年龄、姓名的顺序对人员列表进行排序:

            Comparator<Person> comparator = Comparator.comparingLong(Person::getId)
                    .thenComparingInt(Person::getAge)
                    .thenComparing(Person::getName);
            personList.sort(comparator);

3

如果您想根据报告键(ReportKey)先排序,然后是学生编号(Student Number),最后是学校,则需要比较每个字符串而不是将它们连接起来。如果您在字符串中填充空格,使得每个报告键的长度相同等等,那么您的方法可能有效,但这并不值得努力。相反,只需更改比较方法以比较报告键(ReportKeys),如果compareTo返回0,则尝试学生编号,然后是学校。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接