views:

262

answers:

4

I'm writing some custom Comparators, and I'd like them to push null items to the bottom of the list, regardless of whether I'm sorting ascending or descending. What's a good strategy or pattern for approaching this?

Offhand:

  • simply write separate ascending and descending comparators, sharing code where possible;
  • delegate null handling to another class, either by throwing an NPE or by calling it explicitly;
  • include an ascending flag and put conditional logic in it to navigate around the nulls.
  • Wrap regular comparators in a null-handling class

Any others? I'd like to hear about any experiences with different approaches, any pitfalls for the various strategies.

+3  A: 

The last option appeals to me a lot. Comparators are really great to chain together. In particular you may well want to write a ReverseComparator as well as a NullWrappingComparator.


EDIT: You don't have to write this yourself. If you look at the Ordering class in the Google Collections Library you'll find this and all kinds of other goodies :)


EDIT: Going into more detail to show what I mean about ReverseComparator...

One word of warning - in the implementation of a ReverseComparator, reverse the order of the arguments instead of negating the result, as otherwise Integer.MIN_VALUE is "reversed" to itself.

So this implementation is wrong (assuming original is the comparator to reverse):

public int compare(T x, T y)
{
    return -original.compare(x, y);
}

but this is right:

public int compare(T x, T y)
{
    return original.compare(y, x);
}

The reason is that we always want to reverse the comparison, but if original.compare(x, y) returns int.MIN_VALUE, then the bad comparer will also return int.MIN_VALUE, which is incorrect. This is due to the funny property that int.MIN_VALUE == -int.MIN_VALUE.

Jon Skeet
I agree with all of your answer except about negating the result of a compare; I'd be fairly unhappy if I found out that a Comparator was implementing integer comparison using subtraction, since that method has so many pitfalls.
jprete
@jprete: I think you misunderstood me. I'll edit.
Jon Skeet
I'm accepting this answer because of the reference to Google Collections' Ordering class - existing proven code is the best solution. Also for the warning about Integer.MIN_VALUE. But I greatly appreciate @dfa's code, below, and wish I could accept both answers.
Carl Manaster
+3  A: 

I agree with Jon Skeet (it's so easy :). I tried to implement a very simple decorator:

class NullComparators {

    static <T> Comparator<T> atEnd(final Comparator<T> comparator) {
        return new Comparator<T>() {

            public int compare(T o1, T o2) {
                if (o1 == null && o2 == null) {
                    return 0;
                }

                if (o1 == null) {
                    return 1;
                }

                if (o2 == null) {
                    return -1;
                }

                return comparator.compare(o1, o2);
            }
        };
    }

    static <T> Comparator<T> atBeginning(final Comparator<T> comparator) {
        return Collections.reverseOrder(atEnd(comparator));
    }
}

given a Comparator:

Comparator<String> wrapMe = new Comparator<String>() {
      public int compare(String o1, String o2) {
          return o1.compareTo(o2);
      }
};

and some test data:

List<String> strings = Arrays.asList(null, "aaa", null, "bbb", "ccc", null);

you can sort with nulls at end:

Collections.sort(strings, NullComparators.atEnd(wrapMe));
[aaa, bbb, ccc, null, null, null]

or at beginning:

Collections.sort(strings, NullComparators.atBeginning(wrapMe));
[null, null, null, ccc, bbb, aaa]
dfa
Very nice! Thank you. Is there a reason for not returning 0 if both arguments are null? I know that null behavior is not quite like non-null behavior, and that saying two nulls are equal is questionable - but is it less questionable to say that one exceeds the other?
Carl Manaster
@Carl: Exactly the point I just made to your post :) A Comparator *should* return 0 or throw an exception when passed two nulls, otherwise it's disobeying the interface contract.
Jon Skeet
+2  A: 

Following up on dfa's answer - what I want is that the nulls sort at the end without affecting the order of the non-nulls. So I want something more along the lines of this:

public class NullComparatorsTest extends TestCase {
    Comparator<String> forward = new Comparator<String>() {
            public int compare(String a, String b) {
             return a.compareTo(b);
            }
           };

    public void testIt() throws Exception {
     List<String> strings = Arrays.asList(null, "aaa", null, "bbb", "ccc", null);
     Collections.sort(strings, NullComparators.atEnd(forward));
     assertEquals("[aaa, bbb, ccc, null, null, null]", strings.toString());
     Collections.sort(strings, NullComparators.atBeginning(forward));
     assertEquals("[null, null, null, aaa, bbb, ccc]", strings.toString());
    }
}

public class NullComparators {
    public static <T> Comparator<T> atEnd(final Comparator<T> comparator) {
     return new Comparator<T>() {
      public int compare(T a, T b) {
       if (a == null && b == null)
        return 0;
       if (a == null)
        return 1;
       if (b == null)
        return -1;
       return comparator.compare(a, b);
      }
     };
    }

    public static <T> Comparator<T> atBeginning(final Comparator<T> comparator) {
     return new Comparator<T>() {
      public int compare(T a, T b) {
       if (a == null && b == null)
        return 0;
       if (a == null)
        return -1;
       if (b == null)
        return 1;
       return comparator.compare(a, b);
      }
     };
    }
}

Full credit to dfa, though - this is just a minor modification of his work.

Carl Manaster
One problem: you don't return 0 when comparing two nulls.
Jon Skeet
Thanks; edited with correction.
Carl Manaster
+1  A: 

You could always use NullComparator from commons-collections. It's been around longer than Google Collections.

Tim R