Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nice general way to sort nulls to the bottom, regardless?

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 other strategies? I'd like to hear about any experiences with different approaches, and any pitfalls for the various strategies.

like image 602
Carl Manaster Avatar asked Aug 11 '09 17:08

Carl Manaster


4 Answers

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]
like image 61
dfa Avatar answered Sep 21 '22 12:09

dfa


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.

like image 25
Jon Skeet Avatar answered Sep 21 '22 12:09

Jon Skeet


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.

like image 30
Carl Manaster Avatar answered Sep 19 '22 12:09

Carl Manaster


In Java 8, you can use the Comparator.nullsLast and Comparator.nullsFirst static methods to have more null-friendly comparators. Suppose you have a Fruit class like the following:

public class Fruit {
    private final String name;
    private final Integer size;

    // Constructor and Getters
}

If you want to sort a bunch of fruits by their size and put the nulls at the end:

List<Fruit> fruits = asList(null, new Fruit("Orange", 25), new Fruit("Kiwi", 5));

You can simply write:

Collections.sort(fruits, Comparator.nullsLast(Comparator.comparingInt(Fruit::getSize)));

And the result would be:

[Fruit{name='Kiwi', size=5}, Fruit{name='Orange', size=25}, null]
like image 45
Ali Dehghani Avatar answered Sep 18 '22 12:09

Ali Dehghani