Search code examples
javaandroidcomparator

Add additional rules to the compare method of a Comparator


I currently have a code snippet which returns strings of a list in ascending order:

Collections.sort(myList, new Comparator<MyClass>() {
    @Override
    public int compare(MyClass o1, MyClass o2) {
        return o1.aString.compareTo(o2.aString);
    }
});

While it works, I would like to add some custom "rules" to the order to put certain strings to the front. For instance:

if(aString.equals("Hi")){
// put string first in the order
}

if(aString begins with a null character, e.g. " ") {
// put string after Hi, but before the other strings
}

// So the order could be: Hi, _string, a_string, b_string, c_string

Is it possible to customize the sorting of a list with a Comparator like this?


Solution

  • That's possible.

    Using Java 8 features

    You could pass a function to the Comparator.comparing method to define your rules. Note that we simply return integers, the lowest integer for the elements which should come first.

    Comparator<MyClass> myRules = Comparator.comparing(t -> {
        if (t.aString.equals("Hi")) {
            return 0;
        }
        else if (t.aString.startsWith(" ")) {
            return 1;
        }
        else {
            return 2;
        }
    });
    

    If you want the remaining elements to be sorted alphabetically, you could use thenComparing(Comparator.naturalOrder()), if your class implements Comparable. Otherwise, you should extract the sort key first:

    Collections.sort(myList, myRules.thenComparing(Comparator.comparing(t -> t.aString)));
    

    Note that the actual specific numbers returned don't matter, what matters is that lower numbers come before higher numbers when sorting, so if one would always put the string "Hi" first, then the corresponding number should be the lowest returned (in my case 0).

    Using Java <= 7 features (Android API level 21 compatible)

    If Java 8 features are not available to you, then you could implement it like this:

    Comparator<MyClass> myRules = new Comparator<MyClass>() {
    
        @Override
        public int compare(MyClass o1, MyClass o2) {
            int order = Integer.compare(getOrder(o1), getOrder(o2));
            return (order != 0 ? order : o1.aString.compareTo(o2.aString));
        }
    
        private int getOrder(MyClass m) {
            if (m.aString.equals("Hi")) {
                return 0;
            }
            else if (m.aString.startsWith(" ")) {
                return 1;
            }
            else {
                return 2;
            }
        }
    };
    

    And call it like this:

    Collections.sort(list, myRules);
    

    This works as follows: first, both received strings are mapped to your custom ruleset and subtracted from eachother. If the two differ, then the operation Integer.compare(getOrder(o1), getOrder(o2))1 determines the comparison. Otherwise, if both are the same, then the lexiographic order is used for comparison.

    Here is some code in action.


    1 Always use Integer::compare rather than subtracting one from the other, because of the risk of erroneous results due to integer overflow. See here.