How to compare objects by multiple fields

后端 未结 22 2449
暖寄归人
暖寄归人 2020-11-22 00:43

Assume you have some objects which have several fields they can be compared by:

public class Person {

    private String firstName;
    private String lastN         


        
相关标签:
22条回答
  • 2020-11-22 01:14

    For those able to use the Java 8 streaming API, there is a neater approach that is well documented here: Lambdas and sorting

    I was looking for the equivalent of the C# LINQ:

    .ThenBy(...)
    

    I found the mechanism in Java 8 on the Comparator:

    .thenComparing(...)
    

    So here is the snippet that demonstrates the algorithm.

        Comparator<Person> comparator = Comparator.comparing(person -> person.name);
        comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));
    

    Check out the link above for a neater way and an explanation about how Java's type inference makes it a bit more clunky to define compared to LINQ.

    Here is the full unit test for reference:

    @Test
    public void testChainedSorting()
    {
        // Create the collection of people:
        ArrayList<Person> people = new ArrayList<>();
        people.add(new Person("Dan", 4));
        people.add(new Person("Andi", 2));
        people.add(new Person("Bob", 42));
        people.add(new Person("Debby", 3));
        people.add(new Person("Bob", 72));
        people.add(new Person("Barry", 20));
        people.add(new Person("Cathy", 40));
        people.add(new Person("Bob", 40));
        people.add(new Person("Barry", 50));
    
        // Define chained comparators:
        // Great article explaining this and how to make it even neater:
        // http://blog.jooq.org/2014/01/31/java-8-friday-goodies-lambdas-and-sorting/
        Comparator<Person> comparator = Comparator.comparing(person -> person.name);
        comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));
    
        // Sort the stream:
        Stream<Person> personStream = people.stream().sorted(comparator);
    
        // Make sure that the output is as expected:
        List<Person> sortedPeople = personStream.collect(Collectors.toList());
        Assert.assertEquals("Andi",  sortedPeople.get(0).name); Assert.assertEquals(2,  sortedPeople.get(0).age);
        Assert.assertEquals("Barry", sortedPeople.get(1).name); Assert.assertEquals(20, sortedPeople.get(1).age);
        Assert.assertEquals("Barry", sortedPeople.get(2).name); Assert.assertEquals(50, sortedPeople.get(2).age);
        Assert.assertEquals("Bob",   sortedPeople.get(3).name); Assert.assertEquals(40, sortedPeople.get(3).age);
        Assert.assertEquals("Bob",   sortedPeople.get(4).name); Assert.assertEquals(42, sortedPeople.get(4).age);
        Assert.assertEquals("Bob",   sortedPeople.get(5).name); Assert.assertEquals(72, sortedPeople.get(5).age);
        Assert.assertEquals("Cathy", sortedPeople.get(6).name); Assert.assertEquals(40, sortedPeople.get(6).age);
        Assert.assertEquals("Dan",   sortedPeople.get(7).name); Assert.assertEquals(4,  sortedPeople.get(7).age);
        Assert.assertEquals("Debby", sortedPeople.get(8).name); Assert.assertEquals(3,  sortedPeople.get(8).age);
        // Andi     : 2
        // Barry    : 20
        // Barry    : 50
        // Bob      : 40
        // Bob      : 42
        // Bob      : 72
        // Cathy    : 40
        // Dan      : 4
        // Debby    : 3
    }
    
    /**
     * A person in our system.
     */
    public static class Person
    {
        /**
         * Creates a new person.
         * @param name The name of the person.
         * @param age The age of the person.
         */
        public Person(String name, int age)
        {
            this.age = age;
            this.name = name;
        }
    
        /**
         * The name of the person.
         */
        public String name;
    
        /**
         * The age of the person.
         */
        public int age;
    
        @Override
        public String toString()
        {
            if (name == null) return super.toString();
            else return String.format("%s : %d", this.name, this.age);
        }
    }
    
    0 讨论(0)
  • 2020-11-22 01:16

    Another option you can always consider is Apache Commons. It provides a lot of options.

    import org.apache.commons.lang3.builder.CompareToBuilder;
    

    Ex:

    public int compare(Person a, Person b){
    
       return new CompareToBuilder()
         .append(a.getName(), b.getName())
         .append(a.getAddress(), b.getAddress())
         .toComparison();
    }
    
    0 讨论(0)
  • 2020-11-22 01:16

    Its easy to do using Google's Guava library.

    e.g. Objects.equal(name, name2) && Objects.equal(age, age2) && ...

    More examples:

    • https://stackoverflow.com/a/5039178/1180621
    0 讨论(0)
  • 2020-11-22 01:17

    With Java 8:

    Comparator.comparing((Person p)->p.firstName)
              .thenComparing(p->p.lastName)
              .thenComparingInt(p->p.age);
    

    If you have accessor methods:

    Comparator.comparing(Person::getFirstName)
              .thenComparing(Person::getLastName)
              .thenComparingInt(Person::getAge);
    

    If a class implements Comparable then such comparator may be used in compareTo method:

    @Override
    public int compareTo(Person o){
        return Comparator.comparing(Person::getFirstName)
                  .thenComparing(Person::getLastName)
                  .thenComparingInt(Person::getAge)
                  .compare(this, o);
    }
    
    0 讨论(0)
  • 2020-11-22 01:17

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

    Working code in this gist

    Using Java 8 lambda's (added April 10, 2019)

    Java 8 solves this nicely by lambda's (though Guava and Apache Commons might still offer more flexibility):

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

    Thanks to @gaoagong's answer below.

    Messy and convoluted: Sorting by hand

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

    This requires a lot of typing, maintenance and is error prone.

    The reflective way: Sorting with BeanComparator

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

    Obviously this is more concise, but even more error prone as you lose your direct reference to the fields by using Strings instead (no typesafety, auto-refactorings). Now if a field is renamed, the compiler won’t even report a problem. Moreover, because this solution uses reflection, the sorting is much slower.

    Getting there: Sorting with Google Guava’s 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(); 
            */  
        }  
    });  
    

    This is much better, but requires some boiler plate code for the most common use case: null-values should be valued less by default. For null-fields, you have to provide an extra directive to Guava what to do in that case. This is a flexible mechanism if you want to do something specific, but often you want the default case (ie. 1, a, b, z, null).

    Sorting with 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();  
        }  
    });  
    

    Like Guava’s ComparisonChain, this library class sorts easily on multiple fields, but also defines default behavior for null values (ie. 1, a, b, z, null). However, you can’t specify anything else either, unless you provide your own Comparator.

    Thus

    Ultimately it comes down to flavor and the need for flexibility (Guava’s ComparisonChain) vs. concise code (Apache’s CompareToBuilder).

    Bonus method

    I found a nice solution that combines multiple comparators in order of priority on CodeReview in a 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));
        }
    }
    

    Ofcourse Apache Commons Collections has a util for this already:

    ComparatorUtils.chainedComparator(comparatorCollection)

    Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
    
    0 讨论(0)
  • 2020-11-22 01:18

    If you implement the Comparable interface, you'll want to choose one simple property to order by. This is known as natural ordering. Think of it as the default. It's always used when no specific comparator is supplied. Usually this is name, but your use case may call for something different. You are free to use any number of other Comparators you can supply to various collections APIs to override the natural ordering.

    Also note that typically if a.compareTo(b) == 0, then a.equals(b) == true. It's ok if not but there are side effects to be aware of. See the excellent javadocs on the Comparable interface and you'll find lots of great information on this.

    0 讨论(0)
提交回复
热议问题