95

I have a class named Person with multiple properties, for example:

public class Person {
    private int id;
    private String name, address;
    // Many more properties.
}

A lot of Person-objects are stored in an ArrayList<Person>. I want to sort this list by multiple sort parameters, and different from time to time. For instance I might one time want to sort by name ascending and then address descending, and another time just by id descending.

And I don't want to create my own sort methods (i.e., I want to use Collections.sort(personList, someComparator). What is the most elegant solution that achieves this?

Cœur
  • 37,241
  • 25
  • 195
  • 267
runaros
  • 1,821
  • 4
  • 20
  • 25

9 Answers9

194

I think your enum approach is basically sound, but the switch statements really need a more object oriented approach. Consider:

enum PersonComparator implements Comparator<Person> {
    ID_SORT {
        public int compare(Person o1, Person o2) {
            return Integer.valueOf(o1.getId()).compareTo(o2.getId());
        }},
    NAME_SORT {
        public int compare(Person o1, Person o2) {
            return o1.getFullName().compareTo(o2.getFullName());
        }};

    public static Comparator<Person> decending(final Comparator<Person> other) {
        return new Comparator<Person>() {
            public int compare(Person o1, Person o2) {
                return -1 * other.compare(o1, o2);
            }
        };
    }

    public static Comparator<Person> getComparator(final PersonComparator... multipleOptions) {
        return new Comparator<Person>() {
            public int compare(Person o1, Person o2) {
                for (PersonComparator option : multipleOptions) {
                    int result = option.compare(o1, o2);
                    if (result != 0) {
                        return result;
                    }
                }
                return 0;
            }
        };
    }
}

An example of usage (with a static import).

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}
Yishai
  • 90,445
  • 31
  • 189
  • 263
  • 12
    +1 smart use of enums. I like the elegant combination you do with the enums, the "descending" and the "Composite". I guess the null values treatment is missing, but it's easy to add the same way as "descending". – KLE Sep 15 '09 at 07:48
  • 1
    Many nice answers providing food for thought. Since no answer stood out as the clear alternative, I'm gonna accept this one because I like the elegance, but I urge anyone viewing this answer to check the other approaches as well. – runaros Sep 15 '09 at 21:31
  • @Cosmin, if you have a specific question, you could ask it on StackOverflow as a new question referencing this answer. – Yishai Jul 22 '11 at 16:56
  • This is realy cool explanation, one thing I dint get is **return -1 * other.compare(o1, o2);** line. How it works ? Can anybody please tell me ? – TheLittleNaruto Dec 04 '13 at 07:41
  • 1
    @TheLittleNaruto, the compare method returns a negative number if o2 is greater, a positive one if o1 is greater, and zero if they are equal. Multiplying by -1 reverses the result, which is the idea of decending (the opposite of the usual ascending order), while leaving it as zero if they were equal. – Yishai Dec 04 '13 at 14:05
  • @KLE For handling nulls values, I would do this: `return o1.getId() == null ? -1 : o2.getId() == null ? +1 : Integer.valueOf(o1.getId()).compareTo(o2.getId());` And this: `return o1.getFullName() == null ? -1 : o2.getFullName() == null ? +1 : o1.getFullName().compareTo(o2.getFullName());` – PFROLIM Feb 13 '14 at 18:38
  • fyi: Your `descending` method is already present in standard JDK since 1.5: `Collections#reverseOrder(Comparator)`. – qqilihq Oct 02 '14 at 07:47
  • 5
    Note that since Java 8 you can use `comparator.reversed()` for descending and you can use `comparator1.thenComparing(comparator2)` for chaining comparators. – GuiSim Jan 09 '15 at 16:33
  • @Yishai i dont understand the other.compare(o1, o2) in decending method. If we again call other.compare(o1, o2) doesnt it call again compare method which inside there is a multipleOptions iteration – Harshana Apr 28 '15 at 04:21
  • @Harshana, if I understand your question correctly, the answer is that descending is an independent static comparator, it isn't part of the chain. – Yishai Apr 28 '15 at 17:45
  • @Yishai Sorry Yishai. But you call the getComparator() and pass that comparator return to descending method which it refer by "other" reference inside decending method. So i thought "other" variable point to comparator object which creates inside getComparator method. Can you please clarify this concept to me? – Harshana Apr 28 '15 at 18:08
  • @Harshana, yes, that is correct. The other method has the getComparator, which has two comparators in it, in a chain. That is in turn, wrapped in the descending comparator. So the chain goes -> decending which calls the anonymous comparitor from getComparator, which in turn delegates to the NAME_SORT. If that returns zero, it asks the ID_SORT. The decending comparator then reverses the result of that and returns it to the Collections.sort method. – Yishai Apr 28 '15 at 18:17
  • What would be the complexity of this sort of chaining of comparators? Are we essentially sorting each time we chain the comparators? So we do a n*log(n) operation for each comparator? – John Baum Nov 10 '15 at 16:54
  • 1
    @JohnBaum, if the first comparator returns a non zero result, that result is returned and the rest of the chain is not executed. – Yishai Nov 10 '15 at 17:06
  • Ah, so is it the same as having the multiple comparators logic all in one compare() method that does if/else checks for what to compare when the result is non-zero? – John Baum Nov 10 '15 at 18:09
  • 1
    @JohnBaum, you could get the same result that way. – Yishai Nov 10 '15 at 18:36
  • @FedericoPiazza, I would need a more specific question to be able to address something in a comment. – Yishai Jun 19 '19 at 22:06
  • @Yishai, I meant the syntax you use like `ID_SORT {...}, NAME_SORT {...}`. I'm not sure what you are doing there. How are you using the anonymous blocks? – Federico Piazza Jun 19 '19 at 22:40
  • 1
    @FedericoPiazza, that syntax defines methods on the enum object. They are usually anonymous, but since the enum implements an interface, it is implementing that interface. An Enum is a kind of special instance of an anonymous type that subclasses the Enum class. – Yishai Jun 20 '19 at 15:09
25

You can create comparators for each of properties you might want to sort and then try "comparator chaining" :-) like this:

public class ChainedComparator<T> implements Comparator<T> {
    private List<Comparator<T>> simpleComparators; 
    public ChainedComparator(Comparator<T>... simpleComparators) {
        this.simpleComparators = Arrays.asList(simpleComparators);
    }
    public int compare(T o1, T o2) {
        for (Comparator<T> comparator : simpleComparators) {
            int result = comparator.compare(o1, o2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }
}
Tadeusz Kopec for Ukraine
  • 12,283
  • 6
  • 56
  • 83
16

One way is to create a Comparator that takes as arguments a list of properties to sort by, as this example shows.

public class Person {
    private int id;
    private String name, address;

    public static Comparator<Person> getComparator(SortParameter... sortParameters) {
        return new PersonComparator(sortParameters);
    }

    public enum SortParameter {
        ID_ASCENDING, ID_DESCENDING, NAME_ASCENDING,
        NAME_DESCENDING, ADDRESS_ASCENDING, ADDRESS_DESCENDING
    }

    private static class PersonComparator implements Comparator<Person> {
        private SortParameter[] parameters;

        private PersonComparator(SortParameter[] parameters) {
            this.parameters = parameters;
        }

        public int compare(Person o1, Person o2) {
            int comparison;
            for (SortParameter parameter : parameters) {
                switch (parameter) {
                    case ID_ASCENDING:
                        comparison = o1.id - o2.id;
                        if (comparison != 0) return comparison;
                        break;
                    case ID_DESCENDING:
                        comparison = o2.id - o1.id;
                        if (comparison != 0) return comparison;
                        break;
                    case NAME_ASCENDING:
                        comparison = o1.name.compareTo(o2.name);
                        if (comparison != 0) return comparison;
                        break;
                    case NAME_DESCENDING:
                        comparison = o2.name.compareTo(o1.name);
                        if (comparison != 0) return comparison;
                        break;
                    case ADDRESS_ASCENDING:
                        comparison = o1.address.compareTo(o2.address);
                        if (comparison != 0) return comparison;
                        break;
                    case ADDRESS_DESCENDING:
                        comparison = o2.address.compareTo(o1.address);
                        if (comparison != 0) return comparison;
                        break;
                }
            }
            return 0;
        }
    }
}

It can then be used in code for instance like this:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);
runaros
  • 1,821
  • 4
  • 20
  • 25
  • Yes. If you want your code to be very generic, your enum could specify only the property to read (you could use reflection to get the property using the enum name), and you could specify the rest with a second enum: ASC & DESC, and possibly a third (NULL_FIRST or NULL_LAST). – KLE Sep 14 '09 at 13:06
7

One approach would be to compose Comparators. This could be a library method (I'm sure it exists somewhere out there).

public static <T> Comparator<T> compose(
    final Comparator<? super T> primary,
    final Comparator<? super T> secondary
) {
    return new Comparator<T>() {
        public int compare(T a, T b) {
            int result = primary.compare(a, b);
            return result==0 ? secondary.compare(a, b) : result;
        }
        [...]
    };
}

Use:

Collections.sort(people, compose(nameComparator, addressComparator));

Alternatively, note that Collections.sort is a stable sort. If performance isn't absolutely crucial, you sort be the secondary order before the primary.

Collections.sort(people, addressComparator);
Collections.sort(people, nameComparator);
Tom Hawtin - tackline
  • 145,806
  • 30
  • 211
  • 305
  • Clever approach, however, could it be made more generic, such that it includes a variable number of comparators, possibly including zero? – runaros Sep 14 '09 at 14:39
  • `compose(nameComparator, compose(addressComparator, idComparator))` That would read a little better if Java had extension methods. – Tom Hawtin - tackline Sep 14 '09 at 15:08
3

Comparators lets you do that very easily and naturally. You can create single instances of comparators, either in your Person class itself, or in a Service class associated to your need.
Examples, using anonymous inner classes:

    public static final Comparator<Person> NAME_ASC_ADRESS_DESC
     = new Comparator<Person>() {
      public int compare(Person p1, Person p2) {
         int nameOrder = p1.getName().compareTo(p2.getName);
         if(nameOrder != 0) {
           return nameOrder;
         }
         return -1 * p1.getAdress().comparedTo(p2.getAdress());
         // I use explicit -1 to be clear that the order is reversed
      }
    };

    public static final Comparator<Person> ID_DESC
     = new Comparator<Person>() {
      public int compare(Person p1, Person p2) {
         return -1 * p1.getId().comparedTo(p2.getId());
         // I use explicit -1 to be clear that the order is reversed
      }
    };
    // and other comparator instances as needed... 

If you have many, you can also structure your comparators code any way you like. For example, you could:

  • inherit from another comparator,
  • have a CompositeComparator that agregates some existing comparators
  • have a NullComparator that handle null cases, then delegates to another comparator
  • etc...
KLE
  • 23,689
  • 4
  • 56
  • 62
2

I think coupling the sorters to the Person class, like in your answer, isn't a good idea, because it couples the comparison (usually business driven) and the model object to close to each other. Each time you want to change/add something the sorter, you need to touch the person class, which is usually something you do not want to do.

Using a Service or something similar, which provides Comparator instances, like KLE proposed, sounds way more flexible and extensible.

  • As for me this leads to tight coupling because somehow the comparators holder class HAS to know the detailed data structure of a Person class (basically which fields of Person class to compare) and if you ever gonna change something in Persons fields this leads to trace same changes in comparators class. I guess Person comparators should be a part of a Person class. http://blog.sanaulla.info/2008/06/26/cohesion-and-coupling-two-oo-design-principles/ – Stan Jul 27 '14 at 15:48
2

My approach is build on Yishai's. The main gap is that there is no way to sort first ascending for an attribute and after that decending for an other one. This can not be done with enumerations. For that I used classes. Because the SortOrder strongly depends on the type I prefered to implement it as an inner class of person.

The class 'Person' with inner class 'SortOrder':

import java.util.Comparator;

public class Person {
    private int id;
    private String firstName; 
    private String secondName;

    public Person(int id, String firstName, String secondName) {
        this.id = id;
        this.firstName = firstName;
        this.secondName = secondName;   
    }

    public abstract static class SortOrder implements Comparator<Person> {
        public static SortOrder PERSON_ID = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return Integer.valueOf(p1.getId()).compareTo(p2.getId());
            }
        };
        public static SortOrder PERSON_FIRST_NAME = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return p1.getFirstName().compareTo(p2.getFirstName());
            }
        };
        public static SortOrder PERSON_SECOND_NAME = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return p1.getSecondName().compareTo(p2.getSecondName());
            }
        };

        public static SortOrder invertOrder(final SortOrder toInvert) {
            return new SortOrder() {
                public int compare(Person p1, Person p2) {
                    return -1 * toInvert.compare(p1, p2);
                }
            };
        }

        public static Comparator<Person> combineSortOrders(final SortOrder... multipleSortOrders) {
            return new Comparator<Person>() {
                public int compare(Person p1, Person p2) {
                    for (SortOrder personComparator: multipleSortOrders) {
                        int result = personComparator.compare(p1, p2);
                        if (result != 0) {
                            return result;
                        }
                    }
                    return 0;
                }
            };
        }
    }

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getSecondName() {
        return secondName;
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();

        result.append("Person with id: ");
        result.append(id);
        result.append(" and firstName: ");
        result.append(firstName);
        result.append(" and secondName: ");
        result.append(secondName);
        result.append(".");

        return result.toString();
    }
}

An example for using the class Person and its SortOrder:

import static multiplesortorder.Person.SortOrder.*;

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

import multiplesortorder.Person;

public class Application {

    public static void main(String[] args) {
        List<Person> listPersons = new ArrayList<Person>(Arrays.asList(
                 new Person(0, "...", "..."),
                 new Person(1, "...", "...")
             ));

         Collections.sort(listPersons, combineSortOrders(PERSON_FIRST_NAME, invertOrder(PERSON_ID)));

         for (Person p: listPersons) {
             System.out.println(p.toString());
         }
    }
}

oRUMOo

oRUMOo
  • 153
  • 1
  • 2
  • 11
  • What would be the complexity of this sort of chaining of comparators? Are we essentially sorting each time we chain the comparators? So we do a n*log(n) operation for each comparator? – John Baum Nov 10 '15 at 16:53
0

I recently wrote a Comparator to sort multiple fields within a delimited String record. It allows you to define the delimiter, record structure and sorting rules (some of which are type-specific). You can use this by converting a Person record into a delimited String.

Required information is seeded to the Comparator itself, either programmatically or through an XML file.

XML is validated by a package embedded XSD file. For example, below is a tab delimited record layout with four fields (two of which are sortable):

<?xml version="1.0" encoding="ISO-8859-1"?> 
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <delimiter>&#009;</delimiter>

    <column xsi:type="Decimal">
        <name>Column One</name>
    </column>

    <column xsi:type="Integer">
        <name>Column Two</name>
    </column>

    <column xsi:type="String">
        <name>Column Three</name>
        <sortOrder>2</sortOrder>
        <trim>true</trim>
        <caseSensitive>false</caseSensitive>        
        <stripAccents>true</stripAccents>
    </column>

    <column xsi:type="DateTime">
        <name>Column Four</name>
        <sortOrder>1</sortOrder>
        <ascending>true</ascending>
        <nullLowSortOrder>true</nullLowSortOrder>
        <trim>true</trim>
        <pattern>yyyy-MM-dd</pattern>
    </column>

</row>

You would then use this in java like so:

Comparator<String> comparator = new RowComparator(
              new XMLStructureReader(new File("layout.xml")));

Library can be found here:

http://sourceforge.net/projects/multicolumnrowcomparator/

Constantin
  • 1,506
  • 10
  • 16
0

Suppose a class Coordinate is there and one has to sort it in both ways according to X-coordinate and Y-coordinate. Two differnet comparators is needed for it. Below is the sample

class Coordinate
{

    int x,y;

    public Coordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }

    static Comparator<Coordinate> getCoordinateXComparator() {
        return new Comparator<Coordinate>() {

            @Override
            public int compare(Coordinate Coordinate1, Coordinate Coordinate2) {
                if(Coordinate1.x < Coordinate2.x)
                    return 1;
                else
                    return 0;
            }
            // compare using Coordinate x
        };
    }

    static Comparator<Coordinate> getCoordinateYComparator() {
        return new Comparator<Coordinate>() {

            @Override
            public int compare(Coordinate Coordinate1, Coordinate Coordinate2) {
                if(Coordinate1.y < Coordinate2.y)
                    return 1;
                else
                    return 0;
            }
            // compare using Coordinate y
        };
    }
}
ravi ranjan
  • 5,920
  • 2
  • 19
  • 18