tags:

views:

1108

answers:

6

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?

+4  A: 

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
You appear to have answered this yourself immediately after answering the question?!
Brian Agnew
KLE
I smell abuse. This would deserve a down vote...
Pascal Thivent
At least his own answer should be community wiki... I think it's not the idea of SO to build up reputation by showing smartness (don't get me wrong, the answer is worth every upvote!) on answering your own questions. BTW - thought, you have to wait 2 days before you can answer own questions !?
Andreas_D
@Pascal, I don't agree, the FAQ specifically encourages answering your own question.
Yishai
@Andreas_D, you have to wait two days before accepting your own answer (clicking the check mark).
Yishai
I answered it myself because that is a possible solutions. I'm sure there are better solutions, hence the question.
runaros
Additionally, I don't mind improvements to my solution.
runaros
@Yishai Agreed. But to me, there is something wrong with building up reputation on your own already solved problems. Here, the author could have clearly stated in the question that he already had a solution and was looking for other ideas.
Pascal Thivent
@Pascal See this post for the official view: http://meta.stackoverflow.com/questions/2706
runaros
@runaros I've seen Jeff's opinion... and lots of others. But nothing I'd call an "official way to think". Anyway, I've expressed my point of view and don't want to troll further. Have fun.
Pascal Thivent
+3  A: 

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
+3  A: 

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 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
Clever approach, however, could it be made more generic, such that it includes a variable number of comparators, possibly including zero?
runaros
`compose(nameComparator, compose(addressComparator, idComparator))` That would read a little better if Java had extension methods.
Tom Hawtin - tackline
+2  A: 

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.

pimpf0r
Thanks for the pointer.
KLE
Sound reasonable, thanks for the input :)
runaros
+3  A: 

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

public class ChainedComparator<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
You are probably going to get a warning when that is used (although in JDK7 you should be able to suppress it).
Tom Hawtin - tackline
I really really like this one.
pimpf0r
I like this as well. Can you provide a sample on how to use this with the given example?
runaros
+3  A: 

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
+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
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