I've got a collection of Duck objects and I'd like to sort them using multiple keys.

class Duck {
    DuckAge age; //implements Comparable
    DuckWeight weight; //implements Comparable
    String name;
}
List<Duck> ducks = Pond.getDucks();

eg. I want to sort them primarily by their weights, and secondarily by their age. If two ducks have the exact same weight and the exact same age, then let's differentiate them using their names as a tertiary key. I might do something like this:

Collections.sort(ducks, new Comparator<Duck>(){
    @Override
    public int compare(Duck d1, Duck d2){
        int weightCmp = d1.weight.compareTo(d2.weight);
        if (weightCmp != 0) {
            return weightCmp;
        }
        int ageCmp = d1.age.compareTo(d2.age);
        if (ageCmp != 0) {
            return ageCmp;
        }
        return d1.name.compareTo(d2.name);
    }
});

Well I do this quite frequently, but this solution doesn't smell right. It doesn't scale well, and it's easy to mess up. Surely there must be a better way of sorting Ducks using multiple keys! Does anybody know of a better solution?

EDIT removed unnecessary else branches

share|improve this question
5  
it doesn't look too bad, you can remove one level of indentation, by removing both else since in the if you return, so it's not needed. – stivlo Nov 7 '11 at 12:24
I think @stivlo's right. – Heisenbug Nov 7 '11 at 12:26
7  
+1 for getting your ducks in order – Rich Nov 7 '11 at 15:38
Is there no elegant solution in core Java itself? – SidCool Nov 7 '11 at 16:06

6 Answers

up vote 31 down vote accepted

It doesn't look too bad. Using Guava, you might simply do this:

return ComparisonChain.start()
     .compare(d1.weight, d2.weight)
     .compare(d1.age, d2.age)
     .compare(d1.name, d2.name)
     .result();

which is more elegant. Apache commons-lang has a similar construct (CompareToBuilder).

share|improve this answer
nice, more elegant indeed +1 – stivlo Nov 7 '11 at 12:30
ohh awesome awesome awesome.... +1 – dj aqeel Nov 7 '11 at 12:32
thanks, this is exactly what I wanted! Guava Ordering class looks nice as well, if you already have some Comparators and want to combine them and use them for sorting. – andras Nov 7 '11 at 12:49
Apache's CompareToBuilder is a tiny bit more elegant, since it also handles nulls by default, using a nulls first comparison. Guava's ComparisonChain will throw a NullPointerException unless you add a third parameter (Ordering.natural().nullsFirst()) to every .compare() call. – Daniel Alexiuc Jan 17 '12 at 0:40
If, y'know, you like nulls. – Louis Wasserman Mar 16 '12 at 22:40

Firstly, your solution isn't that slow.

If you really want another method, then give each duck a "score" which is essentially a single number that is the sum of their three characteristics, but with a huge weighting (excuse the almost unavoidable pun) for weight, a lesser one for age; and a very small one for the name.

You can allocate ~10 bits for each characteristic, so for each characteristic you have to be in the range 0..1023.

score = ( (weight << 10) + age) << 10 + name;

This is probably completely unneeded, but whatever :)

share|improve this answer
Nice little trick, thanks. I was going for beauty, not performance, but I'll keep this in mind:) – andras Nov 7 '11 at 12:57
List<Duck> ducks = new ArrayList<Duck>();
Collections.sort(ducks, new Comparator<Duck>() {

  @Override
  public int compare(Duck o1, Duck o2) {

    return new org.apache.commons.lang.builder.CompareToBuilder().
        append(o1.weight, o2.weight).
        append(o1.age, o2.age).
        append(o1.name, o2.name).
        toComparison();
  }
});
share|improve this answer

I have just rewritten your code without nested else statements. Do you like it now?

@Override
public int compare(Duck d1, Duck d2){
    int weightCmp = d1.weight.compareTo(d2.weight);
    if (weightCmp != 0) {
        return weightCmp;
    }
    int ageCmp = d1.age.compareTo(d2.age);
    if (ageCmp != 0) {
        return ageCmp;
    } 

    return d1.name.compareTo(d2.age);
}
share|improve this answer
Yeah, thanks, it does look better, but the main problem was that I was chaining comparisons manually. The Guava ComparisonChain and Apache CompareToBuilder look much better. – andras Nov 7 '11 at 12:54

You can use chained BeanComparators from Commons BeanUtils:

Comparator comparator = new BeanComparator("weight", new BeanComparator("age"));

http://commons.apache.org/beanutils/v1.8.3/apidocs/org/apache/commons/beanutils/BeanComparator.html

share|improve this answer

You can use the CompareToBuilder from Apache Commons Lang. (It explains comparable, but works for Comparator too).

share|improve this answer

Your Answer

 
or
required, but never shown
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.