Collections.sort 具有多个字段

Posted

技术标签:

【中文标题】Collections.sort 具有多个字段【英文标题】:Collections.sort with multiple fields 【发布时间】:2022-01-19 19:16:44 【问题描述】:

我有一个包含三个字段的“报告”对象列表(所有字符串类型)-

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”会发生什么? 固定长度。抱歉忘了提。 Best way to compare objects by multiple fields? 的可能重复项 【参考方案1】:

你发现代码有什么问题吗?

是的。为什么在比较之前将三个字段加在一起?

我可能会做这样的事情:(假设字段按照您希望的顺序排列)

@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;

【讨论】:

请详细说明。那我该怎么做呢?谢谢。 嗨,如果 (c == 0) 可以添加更多?我不知道是否正确,但似乎不正确,因为如果满足第一个条件,则永远不会进入第二个或第三个..等等.. 我不认为你理解a.compareTo(b);约定是值 0 表示相等,负整数表示 a &lt; b 和正整数表示 a &gt; b 对于 Comparable ab 它不工作。我已经尝试过了,但它不起作用。它只适用于一个compareTo【参考方案2】:

如果您想先根据 ReportKey 然后 Student Number 然后 School 进行排序,您需要比较每个 String 而不是连接它们。如果您用空格填充字符串以使每个 ReportKey 具有相同的长度等等,您的方法可能会起作用,但这并不值得付出努力。相反,只需更改 compare 方法来比较 ReportKeys,如果 compareTo 返回 0,则尝试 StudentNumber,然后尝试 School。

【讨论】:

【参考方案3】:

如果您想按报告键、学号、学校进行排序,您应该这样做:

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());
    

当然,这假定所有值都不能为空 - 如果您需要为报告、报告键、学号或学校允许空值,情况会变得更加复杂。

虽然你可以让字符串连接版本使用空格来工作,但如果你有奇怪的数据本身包含空格等,它仍然会在奇怪的情况下失败。上面的代码是 逻辑的 你想要的代码...首先按报告键比较,如果报告键相同,则只考虑学号,等等。

【讨论】:

虽然这段代码没有“错误”,我理解。我更喜欢 Jason 的实现,因为它似乎更容易理解,因为他只有一个 return 语句。 当我尝试使用您的代码时,它无法使用compareTo() 方法。你能帮我解决这个问题吗? @viper:不,因为我没有实现Comparable&lt;T&gt;,我正在实现Comparator&lt;T&gt;。我们不知道您想要达到什么目标,或者您尝试了什么,或者出了什么问题。也许您应该在进行更多研究之后提出一个新问题。 (很可能已经有人问过了。)【参考方案4】:

如果 StudentNumber 是数字,则不会按数字排序,而是按字母数字排序。 不要指望

"2" < "11"

它将是:

"11" < "2"

【讨论】:

这回答了为什么结果错误的实际问题。【参考方案5】:

我会使用Guava 的ComparisonChain 做一个比较器:

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();
  

【讨论】:

【参考方案6】:

(原自Ways to sort lists of objects in Java based on multiple fields)

this gist中的原始工作代码

使用 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));

感谢@gaaogong的answer below。

请注意,这里的一个优点是对 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);  
      
);  

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

反射方式:使用 BeanComparator 排序

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

Collections.sort(pizzas, chain);  

显然这更简洁,但更容易出错,因为您使用字符串代替了对字段的直接引用(没有类型安全,自动重构)。现在,如果一个字段被重命名,编译器甚至不会报告问题。而且,因为这个方案使用了反射,所以排序要慢很多。

到达那里:使用 Google Guava 的比较链进行排序

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(); 
        */  
      
);  

这要好得多,但对于最常见的用例需要一些样板代码:默认情况下,null-values 的值应该较小。对于空字段,您必须为 Guava 在这种情况下做什么提供额外的指令。如果您想做一些特定的事情,这是一种灵活的机制,但通常您需要默认情况(即 1、a、b、z、null)。

如下面的 cmets 所述,每次比较都会立即评估这些 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)。但是,您也不能指定任何其他内容,除非您提供自己的 Comparator。

同样,如下面的 cmets 所述,每次比较都会立即评估这些 getter。

因此

最终归结为风味和灵活性需求(Guava 的 ComparisonChain)与简洁的代码(Apache 的 CompareToBuilder)。

奖励方式

我找到了一个很好的解决方案,它可以在 MultiComparator 中按优先级顺序组合多个比较器 on CodeReview:

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));

【讨论】:

干净代码的完美解决方案,也可以达到目的 感谢本尼的精彩回答。我有一个场景,属性不在我的对象内。但是有一个嵌套对象。在那种情况下我该怎么办?例如,在这里 Collections.sort(reportList, Comparator.comparing(Report::getReportKey) .thenComparing(Report::getStudentNumber) .thenComparing(Report::getSchool));在报告对象内部我有一个学生对象,然后在学生对象内部我有一个学生编号。在这种情况下,我们如何在这里排序?任何帮助将不胜感激。 @MadhuReddy 在示例中,方法引用用作 lambda,但您可以只提供一个适当的 lambda,它返回适当的嵌套字段。 使用chainedComparator 的Bonus Method 非常容易集成。【参考方案7】:

这是一个老问题,所以我看不到 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 + '\'' +
                   '';
        
    

【讨论】:

comparingthenComparing 获胜! 它要求最低 android 版本 24。【参考方案8】:

在 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)); 这段代码帮助了我。谢谢 它要求使用最低 android 版本 24。【参考方案9】:

这是一个比较对象中的 2 个字段的完整示例,一个 String 和一个 int,同样使用 Collat​​or 进行排序。

public class Test 

    public static void main(String[] args) 

        Collator myCollator;
        myCollator = Collator.getInstance(Locale.US);

        List<Item> items = new ArrayList<Item>();

        items.add(new Item("costrels", 1039737, ""));
        items.add(new Item("Costs", 1570019, ""));
        items.add(new Item("costs", 310831, ""));
        items.add(new Item("costs", 310832, ""));

        Collections.sort(items, new Comparator<Item>() 
            @Override
            public int compare(final Item record1, final Item record2) 
                int c;
                //c = record1.item1.compareTo(record2.item1); //optional comparison without Collator                
                c = myCollator.compare(record1.item1, record2.item1);
                if (c == 0) 
                
                    return record1.item2 < record2.item2 ? -1
                            :  record1.item2 > record2.item2 ? 1
                            : 0;
                
                return c;
            
        );     

        for (Item item : items)
        
            System.out.println(item.item1);
            System.out.println(item.item2);
               

    

    public static class Item
    
        public String item1;
        public int item2;
        public String item3;

        public Item(String item1, int item2, String item3)
        
            this.item1 = item1;
            this.item2 = item2;
            this.item3 = item3;
               
    


输出:

成本 1039737

成本 310831

成本 310832

费用 1570019

【讨论】:

【参考方案10】:

我建议使用 Java 8 Lambda 方法:

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

【讨论】:

任何 kotlin 解决方案?【参考方案11】:

Comparator接口与JDK1.8中引入的方法一起使用:comparingthenComparing,或更具体的方法:comparingXXXthenComparingXXX

例如,如果我们想先按 id,然后按年龄,然后按姓名对人员列表进行排序:

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

【讨论】:

【参考方案12】:

上面的很多答案都有在单个比较器方法中比较的字段,但实际上并没有工作。虽然为每个字段实现了不同的比较器,但有一些答案,我发布这个是因为我相信这个例子会更清晰,更容易理解。

class Student
    Integer bornYear;
    Integer bornMonth;
    Integer bornDay;
    public Student(int bornYear, int bornMonth, int bornDay) 

        this.bornYear = bornYear;
        this.bornMonth = bornMonth;
        this.bornDay = bornDay;
    
    public Student(int bornYear, int bornMonth) 

        this.bornYear = bornYear;
        this.bornMonth = bornMonth;

    
    public Student(int bornYear) 

        this.bornYear = bornYear;

    
    public Integer getBornYear() 
        return bornYear;
    
    public void setBornYear(int bornYear) 
        this.bornYear = bornYear;
    
    public Integer getBornMonth() 
        return bornMonth;
    
    public void setBornMonth(int bornMonth) 
        this.bornMonth = bornMonth;
    
    public Integer getBornDay() 
        return bornDay;
    
    public void setBornDay(int bornDay) 
        this.bornDay = bornDay;
    
    @Override
    public String toString() 
        return "Student [bornYear=" + bornYear + ", bornMonth=" + bornMonth + ", bornDay=" + bornDay + "]";
    



class TestClass
       

    // Comparator problem in JAVA for sorting objects based on multiple fields 
    public static void main(String[] args)
    
        int N,c;// Number of threads

        Student s1=new Student(2018,12);
        Student s2=new Student(2018,12);
        Student s3=new Student(2018,11);
        Student s4=new Student(2017,6);
        Student s5=new Student(2017,4);
        Student s6=new Student(2016,8);
        Student s7=new Student(2018);
        Student s8=new Student(2017,8);
        Student s9=new Student(2017,2);
        Student s10=new Student(2017,9);

        List<Student> studentList=new ArrayList<>();
        studentList.add(s1);
        studentList.add(s2);
        studentList.add(s3);
        studentList.add(s4);
        studentList.add(s5);
        studentList.add(s6);
        studentList.add(s7);
        studentList.add(s8);
        studentList.add(s9);
        studentList.add(s10);

        Comparator<Student> byMonth=new Comparator<Student>() 
            @Override
            public int compare(Student st1,Student st2) 
                if(st1.getBornMonth()!=null && st2.getBornMonth()!=null) 
                    return st2.getBornMonth()-st1.getBornMonth();
                
                else if(st1.getBornMonth()!=null) 
                    return 1;
                
                else 
                    return -1;
                
        ;

        Collections.sort(studentList, new Comparator<Student>() 
            @Override
            public int compare(Student st1,Student st2) 
                return st2.getBornYear()-st1.getBornYear();
        .thenComparing(byMonth));

        System.out.println("The sorted students list in descending is"+Arrays.deepToString(studentList.toArray()));



    


输出

按降序排列的学生列表为[Student [bornYear=2018,bornMonth=null,bornDay=null], Student [bornYear=2018,bornMonth=12,bornDay=null], Student [bornYear=2018,bornMonth=12 ,bornDay=null],学生[bornYear=2018,bornMonth=11,bornDay=null],学生[bornYear=2017,bornMonth=9,bornDay=null],学生[bornYear=2017,bornMonth=8,bornDay=null] , 学生 [bornYear=2017,bornMonth=6,bornDay=null], 学生 [bornYear=2017,bornMonth=4,bornDay=null], 学生 [bornYear=2017, BornMonth=2, BornDay=null], 学生 [bornYear= 2016,bornMonth=8,bornDay=null]]

【讨论】:

【参考方案13】:

我遇到了同样的问题,我需要一个使用配置文件的算法。这样,您可以使用配置文件定义的多个字段(仅通过 List

  public static void test()   
    // Associate your configName with your Comparator 
    Map<String, Comparator<DocumentDto>> map = new HashMap<>();
    map.put("id", new IdSort());
    map.put("createUser", new DocumentUserSort());
    map.put("documentType", new DocumentTypeSort());
    /**
      In your config.yml file, you'll have something like
      sortlist: 
        - documentType
        - createUser
        - id
    */
    List<String> config = new ArrayList<>();
    config.add("documentType");
    config.add("createUser");
    config.add("id");
    

    List<Comparator<DocumentDto>> sorts = new ArrayList<>();

    for (String comparator : config) 
        sorts.add(map.get(comparator));
    

    // Begin creation of the list 
    DocumentDto d1 = new DocumentDto();
    d1.setDocumentType(new DocumentTypeDto());
    d1.getDocumentType().setCode("A");
    d1.setId(1);
    d1.setCreateUser("Djory");

    DocumentDto d2 = new DocumentDto();
    d2.setDocumentType(new DocumentTypeDto());
    d2.getDocumentType().setCode("A");
    d2.setId(2);
    d2.setCreateUser("Alex");

    DocumentDto d3 = new DocumentDto();
    d3.setDocumentType(new DocumentTypeDto());
    d3.getDocumentType().setCode("A");
    d3.setId(3);
    d3.setCreateUser("Djory");

    DocumentDto d4 = new DocumentDto();
    d4.setDocumentType(new DocumentTypeDto());
    d4.getDocumentType().setCode("A");
    d4.setId(4);
    d4.setCreateUser("Alex");

    DocumentDto d5 = new DocumentDto();
    d5.setDocumentType(new DocumentTypeDto());
    d5.getDocumentType().setCode("D");
    d5.setId(5);
    d5.setCreateUser("Djory");

    DocumentDto d6 = new DocumentDto();
    d6.setDocumentType(new DocumentTypeDto());
    d6.getDocumentType().setCode("B");
    d6.setId(6);
    d6.setCreateUser("Alex");

    DocumentDto d7 = new DocumentDto();
    d7.setDocumentType(new DocumentTypeDto());
    d7.getDocumentType().setCode("B");
    d7.setId(7);
    d7.setCreateUser("Alex");

    List<DocumentDto> documents = new ArrayList<>();

    documents.add(d1);
    documents.add(d2);
    documents.add(d3);
    documents.add(d4);
    documents.add(d5);
    documents.add(d6);
    documents.add(d7);
    // End creation of the list


    // The Sort 
    Stream<DocumentDto> docStream = documents.stream();

    // we need to reverse this list in order to sort by documentType first because stream are pull-based, last sorted() will have the priority
    Collections.reverse(sorts);

    for(Comparator<DocumentDto> entitySort : sorts)
        docStream = docStream.sorted(entitySort);
    
    documents = docStream.collect(Collectors.toList());
    // documents has been sorted has you configured
    // in case of equality second sort will be used.

    System.out.println(documents);

  

比较器对象非常简单。

public class IdSort implements Comparator<DocumentDto> 

    @Override
    public int compare(DocumentDto o1, DocumentDto o2) 
        return o1.getId().compareTo(o2.getId());
    

public class DocumentUserSort implements Comparator<DocumentDto> 

    @Override
    public int compare(DocumentDto o1, DocumentDto o2) 
        return o1.getCreateUser().compareTo(o2.getCreateUser());
    

public class DocumentTypeSort implements Comparator<DocumentDto> 

    @Override
    public int compare(DocumentDto o1, DocumentDto o2) 
        return o1.getDocumentType().getCode().compareTo(o2.getDocumentType().getCode());
    

结论:此方法效率不高,但您可以通过这种方式使用文件配置创建通用排序。

【讨论】:

【参考方案14】:

我的情况是列表列表(在近似示例中):

List<T>.steam
.map(Class1.StaticInnerClass1::Field1)
.flatMap(x -> x.getField11ListStaticInnerClass2OfField1.stream())
.max(Comparator.comparing(Class1.StaticInnerClass2::Field21,Collections.reverseOrder())
.thenCompare(Class1.StaticInnerClass2::Field22));

【讨论】:

以上是关于Collections.sort 具有多个字段的主要内容,如果未能解决你的问题,请参考以下文章

Java:Collections.sort和Arrays.sort的区别

java中排序函数sort()使用,Arrays.sort()和Collections.sort()

为啥 Collections.sort 使用 Mergesort 而 Arrays.sort 不使用?

为啥 Collections.sort 使用合并排序而不是快速排序?

关于Java中Collections.sort和Arrays.sort的稳定性问题

Collections.sort 使用啥设计模式?