Как упростить реализацию null-safe compareTo()?

Я реализую метод compareTo() для простого класса, такого как этот (чтобы использовать Collections.sort() и другие лакомства, предлагаемые платформой Java):

public class Metadata implements Comparable<Metadata> {
    private String name;
    private String value;

// Imagine basic constructor and accessors here
// Irrelevant parts omitted
}

Я хочу, чтобы естественный порядок для этих объектов был: 1) отсортирован по имени и 2) отсортирован по значению, если имя одинаково; оба сравнения должны быть нечувствительны к регистру. Для обоих полей нулевые значения вполне приемлемы, поэтому compareTo не должно ломаться в этих случаях.

Решение, которое приходит в голову, похоже на следующие строки (здесь я использую "защитные предложения", в то время как другие могут предпочесть одну точку возврата, но это рядом с точкой):

// primarily by name, secondarily by value; null-safe; case-insensitive
public int compareTo(Metadata other) {
    if (this.name == null && other.name != null){
        return -1;
    }
    else if (this.name != null && other.name == null){
        return 1;
    }
    else if (this.name != null && other.name != null) {
        int result = this.name.compareToIgnoreCase(other.name);
        if (result != 0){
            return result;
        }
    }

    if (this.value == null) {
        return other.value == null ? 0 : -1;
    }
    if (other.value == null){
        return 1;
    }

    return this.value.compareToIgnoreCase(other.value);
}

Это делает работу, но я не совсем доволен этим кодом. По общему признанию, это не очень сложно, но довольно многословно и утомительно.

Вопрос в том, , как бы вы сделали это менее подробным (сохраняя при этом функциональность)? Если они помогут, обратитесь к стандартным библиотекам Java или Apache Commons. Будет ли единственный вариант сделать это (немного) проще для реализации моего собственного "NullSafeStringComparator" и применить его для сравнения обоих полей?

Редактирование 1-3: Эдди справа; исправлено, что "оба названия имеют нулевой" случай выше

О принятом ответе

Я задал этот вопрос еще в 2009 году, на Java 1.6, конечно, и в то время чистое решение JDK от Eddie было моим предпочтительный принятый ответ. Я так и не смог изменить это до сих пор (2017 год).

Существуют также сторонние библиотечные решения - в 2009 году Apache Commons Collections один и один из двух выпусков Guava в 2013 году, которые я опубликовал, - что я действительно предпочитал в какой-то момент время.

Теперь я сделал чистый Java 8 решение от Lukasz Wiktor принятый ответ. Это должно быть особенно предпочтительным, если на Java 8, и в наши дни Java 8 должен быть доступен почти для всех проектов.

Ответ 1

Использование Java 8:

private static Comparator<String> nullSafeStringComparator = Comparator
        .nullsFirst(String::compareToIgnoreCase); 

private static Comparator<Metadata> metadataComparator = Comparator
        .comparing(Metadata::getName, nullSafeStringComparator)
        .thenComparing(Metadata::getValue, nullSafeStringComparator);

public int compareTo(Metadata that) {
    return metadataComparator.compare(this, that);
}

Ответ 2

Вы можете просто использовать Apache Commons Lang:

result = ObjectUtils.compare(firstComparable, secondComparable)

Ответ 3

Я бы применил нулевой безопасный компаратор. Там может быть реализация, но это так просто реализовать, что я всегда откатывал свои собственные.

Примечание. Ваш компаратор выше, если оба имени равны нулю, даже не сравнивает поля значений. Я не думаю, что это то, что вы хотите.

Я бы выполнил это с помощью следующего:

// primarily by name, secondarily by value; null-safe; case-insensitive
public int compareTo(final Metadata other) {

    if (other == null) {
        throw new NullPointerException();
    }

    int result = nullSafeStringComparator(this.name, other.name);
    if (result != 0) {
        return result;
    }

    return nullSafeStringComparator(this.value, other.value);
}

public static int nullSafeStringComparator(final String one, final String two) {
    if (one == null ^ two == null) {
        return (one == null) ? -1 : 1;
    }

    if (one == null && two == null) {
        return 0;
    }

    return one.compareToIgnoreCase(two);
}

EDIT: Исправлены опечатки в образце кода. Это то, что я получаю за то, что не тестировал его в первую очередь!

EDIT: продвинутый nullSafeStringComparator к static.

Ответ 4

См. нижнюю часть этого ответа для обновленного (2013) решения с использованием Guava.


Это то, с чем я в конечном итоге пошел. Оказалось, что у нас уже был метод утилиты для нулевого безопасного сравнения String, поэтому самым простым решением было воспользоваться этим. (Это большая база кода, легко пропустить такую ​​вещь:)

public int compareTo(Metadata other) {
    int result = StringUtils.compare(this.getName(), other.getName(), true);
    if (result != 0) {
        return result;
    }
    return StringUtils.compare(this.getValue(), other.getValue(), true);
}

Вот как определяется помощник (он перегружен, так что вы также можете определить, будут ли нулевые значения первыми или последними, если хотите):

public static int compare(String s1, String s2, boolean ignoreCase) { ... }

Итак, это по существу то же самое, что Эдди отвечает (хотя я бы не назвал статический вспомогательный метод компаратором) и что ужин тоже.

Во всяком случае, в общем, я бы сильно поддержал решение Patrick, поскольку я считаю хорошей практикой использовать созданные библиотеки, когда это возможно. (Знайте и используйте библиотеки, как говорит Джош Блох.) Но в этом случае это не принесло бы самого чистого и простейшего кода.

Изменить (2009): версия Коллекции коллекций Apache

На самом деле, здесь проще упростить решение, основанное на Apache Commons NullComparator. Объедините его с без учета регистра Comparator в классе String:

public static final Comparator<String> NULL_SAFE_COMPARATOR 
    = new NullComparator(String.CASE_INSENSITIVE_ORDER);

@Override
public int compareTo(Metadata other) {
    int result = NULL_SAFE_COMPARATOR.compare(this.name, other.name);
    if (result != 0) {
        return result;
    }
    return NULL_SAFE_COMPARATOR.compare(this.value, other.value);
}

Теперь это довольно элегантно, я думаю. (Остается только одна небольшая проблема: Commons NullComparator не поддерживает дженерики, поэтому существует непроверенное задание.)

Обновление (2013): версия Guava

Спустя почти 5 лет, вот как я решаю свой первоначальный вопрос. Если кодирование в Java, я бы (конечно) использовал Guava. (И, конечно же, не Apache Commons.)

Поместите эту константу где-нибудь, например. в классе "StringUtils":

public static final Ordering<String> CASE_INSENSITIVE_NULL_SAFE_ORDER =
    Ordering.from(String.CASE_INSENSITIVE_ORDER).nullsLast(); // or nullsFirst()

Тогда в public class Metadata implements Comparable<Metadata>:

@Override
public int compareTo(Metadata other) {
    int result = CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.name, other.name);
    if (result != 0) {
        return result;
    }
    return CASE_INSENSITIVE_NULL_SAFE_ORDER.compare(this.value, other.value);
}    

Конечно, это почти идентично версии Apache Commons (оба используют JDK CASE_INSENSITIVE_ORDER), использование nullsLast() является единственной спецификой Guava. Эта версия предпочтительна просто потому, что Guava предпочтительнее, чем зависимость, от Commons Collections. (Как все согласны.)

Если вам интересно о Ordering, обратите внимание, что он реализует Comparator. Это очень удобно, особенно для более сложных задач сортировки, позволяя вам, например, сгруппировать несколько заказов с помощью compound(). Прочтите Объяснение заказа для большего количества!

Ответ 5

Я всегда рекомендую использовать сообщества Apache, поскольку он, скорее всего, будет лучше, чем тот, который вы можете написать самостоятельно. Кроме того, вы можете выполнять "настоящую" работу, а не изобретать ее повторно.

Класс, который вас интересует, это Null Comparator. Это позволяет вам делать нольвы высокие или низкие. Вы также даете ему свой собственный компаратор для использования, когда два значения не являются нулевыми.

В вашем случае вы можете иметь статическую переменную-член, которая выполняет сравнение, а затем ваш метод compareTo просто ссылается на это.

Somthing like

class Metadata implements Comparable<Metadata> {
private String name;
private String value;

static NullComparator nullAndCaseInsensitveComparator = new NullComparator(
        new Comparator<String>() {

            @Override
            public int compare(String o1, String o2) {
                // inputs can't be null
                return o1.compareToIgnoreCase(o2);
            }

        });

@Override
public int compareTo(Metadata other) {
    if (other == null) {
        return 1;
    }
    int res = nullAndCaseInsensitveComparator.compare(name, other.name);
    if (res != 0)
        return res;

    return nullAndCaseInsensitveComparator.compare(value, other.value);
}

}

Даже если вы решите опрокинуть свой собственный, помните об этом классе, так как это очень полезно при заказе списков, которые содержат нулевые элементы.

Ответ 6

Я знаю, что это может быть не прямой ответ на ваш вопрос, потому что вы сказали, что нужно поддерживать нулевые значения.

Но я просто хочу отметить, что поддержка нулей в compareTo не соответствует контракту compareTo, описанному в официальном javadocs для Comparable:

Обратите внимание, что null не является экземпляром какого-либо класса, а e.compareTo(null) должен вызывать исключение NullPointerException, даже если e.equals(null) возвращает ложь.

Таким образом, я бы либо бросил NullPointerException явным образом, либо просто позволю ему быть выброшенным в первый раз, когда нулевой аргумент разыменовывается.

Ответ 7

Вы можете извлечь метод:

public int cmp(String txt, String otherTxt)
{
    if ( txt == null )
        return otjerTxt == null ? 0 : 1;

    if ( otherTxt == null )
          return 1;

    return txt.compareToIgnoreCase(otherTxt);
}

public int compareTo(Metadata other) {
   int result = cmp( name, other.name); 
   if ( result != 0 )  return result;
   return cmp( value, other.value); 

}

Ответ 8

Вы могли бы создать свой класс для неизменяемости (для эффективной Java 2nd Ed. есть отличный раздел по этому вопросу, пункт 15: минимизация изменчивости) и убедитесь, что при построении нет нулей (и используйте нулевой шаблон объекта, если необходимо). Затем вы можете пропустить все эти проверки и смело предположить, что значения не равны нулю.

Ответ 9

Я искал нечто похожее, и это казалось немного сложным, поэтому я сделал это. Я думаю, это немного легче понять. Вы можете использовать его как компаратор или как один лайнер. Для этого вопроса вы измените на compareToIgnoreCase(). Как и в случае с нулевыми значениями. Вы можете перевернуть 1, -1, если хотите, чтобы они утонули.

StringUtil.NULL_SAFE_COMPARATOR.compare(getName(), o.getName());

.

public class StringUtil {
    public static final Comparator<String> NULL_SAFE_COMPARATOR = new Comparator<String>() {

        @Override
        public int compare(final String s1, final String s2) {
            if (s1 == s2) {
                //Nulls or exact equality
                return 0;
            } else if (s1 == null) {
                //s1 null and s2 not null, so s1 less
                return -1;
            } else if (s2 == null) {
                //s2 null and s1 not null, so s1 greater
                return 1;
            } else {
                return s1.compareTo(s2);
            }
        }
    }; 

    public static void main(String args[]) {
        final ArrayList<String> list = new ArrayList<String>(Arrays.asList(new String[]{"qad", "bad", "sad", null, "had"}));
        Collections.sort(list, NULL_SAFE_COMPARATOR);

        System.out.println(list);
    }
}

Ответ 10

мы можем использовать java 8, чтобы сделать нулевое сравнение между объектом. предположил, что я hava класс Boy с двумя полями: имя строки и целочисленный возраст, и я хочу сначала сравнить имена, а затем возрасты, если оба они равны.

static void test2() {
    List<Boy> list = new ArrayList<>();
    list.add(new Boy("Peter", null));
    list.add(new Boy("Tom", 24));
    list.add(new Boy("Peter", 20));
    list.add(new Boy("Peter", 23));
    list.add(new Boy("Peter", 18));
    list.add(new Boy(null, 19));
    list.add(new Boy(null, 12));
    list.add(new Boy(null, 24));
    list.add(new Boy("Peter", null));
    list.add(new Boy(null, 21));
    list.add(new Boy("John", 30));

    List<Boy> list2 = list.stream()
            .sorted(comparing(Boy::getName, 
                        nullsLast(naturalOrder()))
                   .thenComparing(Boy::getAge, 
                        nullsLast(naturalOrder())))
            .collect(toList());
    list2.stream().forEach(System.out::println);

}

private static class Boy {
    private String name;
    private Integer age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public Boy(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return "name: " + name + " age: " + age;
    }
}

и результат:

    name: John age: 30
    name: Peter age: 18
    name: Peter age: 20
    name: Peter age: 23
    name: Peter age: null
    name: Peter age: null
    name: Tom age: 24
    name: null age: 12
    name: null age: 19
    name: null age: 21
    name: null age: 24

Ответ 11

В случае, если кто-либо использует Spring, существует класс org.springframework.util.comparator.NullSafeComparator, который делает это и для вас. Просто укрась свой собственный, сопоставимый с этим, как это

new NullSafeComparator<YourObject>(new YourComparable(), true)

https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/util/comparator/NullSafeComparator.html

Ответ 12

Другой пример Apache ObjectUtils. Возможность сортировки других типов объектов.

@Override
public int compare(Object o1, Object o2) {
    String s1 = ObjectUtils.toString(o1);
    String s2 = ObjectUtils.toString(o2);
    return s1.toLowerCase().compareTo(s2.toLowerCase());
}

Ответ 13

Использование Java 7:

public int compareNullFirst(String str1, String str2) {
    if (Objects.equals(str1, str2)) {
        return 0;
    }
    else {
        return str1 == null ? -1 : (str2 == null ? 1 : str1.compareToIgnoreCase(str2));
    }
}

Ответ 14

Это моя реализация, которую я использую для сортировки моего массива ArrayList. нулевые классы сортируются до последнего.

для моего случая, EntityPhone расширяет EntityAbstract, а мой контейнер - List <EntityAbstract> .

метод compareIfNull() используется для нулевой безопасной сортировки. Другие методы для полноты, показывая, как compareIfNull можно использовать.

@Nullable
private static Integer compareIfNull(EntityPhone ep1, EntityPhone ep2) {

    if (ep1 == null || ep2 == null) {
        if (ep1 == ep2) {
            return 0;
        }
        return ep1 == null ? -1 : 1;
    }
    return null;
}

private static final Comparator<EntityAbstract> AbsComparatorByName = = new Comparator<EntityAbstract>() {
    @Override
    public int compare(EntityAbstract ea1, EntityAbstract ea2) {

    //sort type Phone first.
    EntityPhone ep1 = getEntityPhone(ea1);
    EntityPhone ep2 = getEntityPhone(ea2);

    //null compare
    Integer x = compareIfNull(ep1, ep2);
    if (x != null) return x;

    String name1 = ep1.getName().toUpperCase();
    String name2 = ep2.getName().toUpperCase();

    return name1.compareTo(name2);
}
}


private static EntityPhone getEntityPhone(EntityAbstract ea) { 
    return (ea != null && ea.getClass() == EntityPhone.class) ?
            (EntityPhone) ea : null;
}

Ответ 15

В конкретном случае, когда вы знаете, что данные не будут иметь нулей (всегда хорошая идея для строк), и данные действительно велики, вы по-прежнему выполняете три сравнения до фактического сравнения значений, если вы точно знаете, что это ваш случай, вы можете оптимизировать бит немного. YMMV как читаемый код превосходит небольшую оптимизацию:

        if(o1.name != null && o2.name != null){
            return o1.name.compareToIgnoreCase(o2.name);
        }
        // at least one is null
        return (o1.name == o2.name) ? 0 : (o1.name != null ? 1 : -1);

Ответ 16

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Comparator;

public class TestClass {

    public static void main(String[] args) {

        Student s1 = new Student("1","Nikhil");
        Student s2 = new Student("1","*");
        Student s3 = new Student("1",null);
        Student s11 = new Student("2","Nikhil");
        Student s12 = new Student("2","*");
        Student s13 = new Student("2",null);
        List<Student> list = new ArrayList<Student>();
        list.add(s1);
        list.add(s2);
        list.add(s3);
        list.add(s11);
        list.add(s12);
        list.add(s13);

        list.sort(Comparator.comparing(Student::getName,Comparator.nullsLast(Comparator.naturalOrder())));

        for (Iterator iterator = list.iterator(); iterator.hasNext();) {
            Student student = (Student) iterator.next();
            System.out.println(student);
        }


    }

}

вывод

Student [name=*, id=1]
Student [name=*, id=2]
Student [name=Nikhil, id=1]
Student [name=Nikhil, id=2]
Student [name=null, id=1]
Student [name=null, id=2]