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

157

Я реализую 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. было моим предпочтительным принятым ответом. Я никогда не удосужился изменить это до сих пор (2017).

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

Я теперь сделал чистое решение Java 8 от Лукаша Виктора принятым ответом. Это определенно должно быть предпочтительным, если на Java 8, и в настоящее время Java 8 должна быть доступна почти для всех проектов.

Jonik
источник
stackoverflow.com/questions/369383/…
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

Ответы:

173

Используя 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);
}
Лукаш Виктор
источник
7
Я поддерживаю использование встроенного Java 8 в пользу Apache Commons Lang, но этот код Java 8 довольно уродлив и все еще многословен. Я буду придерживаться org.apache.commons.lang3.builder.CompareToBuilder на данный момент.
jschreiner
1
Это не работает для Collections.sort (Arrays.asList (null, val1, null, val2, null)), так как он попытается вызвать compareTo () для нулевого объекта. Похоже, проблема со структурой коллекций, если честно, пытаться понять, как с этим справиться.
Педро Борхес
3
@PedroBorges Автор спросил о сортировке контейнерных объектов, которые имеют сортируемые поля (где эти поля могут быть нулевыми), а не о сортировке пустых ссылок на контейнеры. Таким образом, хотя ваш комментарий верен, Collections.sort(List)он не работает, когда список содержит нули, комментарий не имеет отношения к вопросу.
Скрабби
211

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

result = ObjectUtils.compare(firstComparable, secondComparable)
Даг
источник
4
(@Kong: Это заботится о нулевой безопасности, но не учитывает регистр, который был другим аспектом исходного вопроса. Таким образом, не меняя принятый ответ.)
Jonik
3
Кроме того, по моему мнению, Apache Commons не должен быть принятым ответом в 2013 году. (Даже если некоторые подпроекты поддерживаются лучше, чем другие.) Гуава может использоваться для достижения того же самого ; см nullsFirst()/ nullsLast().
Джоник
8
@Jonik Почему вы считаете, что Apache Commons не должен быть принятым ответом в 2013 году?
действительно хорошая
1
Большая часть Apache Commons - это устаревшие / плохо обслуживаемые / некачественные вещи. Существуют лучшие альтернативы для большинства вещей, которые он предоставляет, например, в Guava, который является очень высококачественной библиотекой, и все чаще в самой JDK. Примерно в 2005 году, Apache Commons был дерьмом, но в наши дни в большинстве проектов это не нужно. (Конечно, есть исключения; например, если бы мне нужен был FTP-клиент по какой-то причине, я бы, вероятно, использовал его в Apache Commons Net и т. Д.)
Jonik
6
@Jonik, как бы вы ответили на вопрос, используя Гуава? Ваше утверждение о том, что Apache Commons Lang (пакет org.apache.commons.lang3) является «устаревшим / плохо обслуживаемым / некачественным», является ложным или в лучшем случае необоснованным. Commons Lang3 легко понять и использовать, и он активно поддерживается. Вероятно, это моя наиболее часто используемая библиотека (кроме Spring Framework и Spring Security) - класс StringUtils с его нулевыми безопасными методами, например, делает нормализацию ввода тривиальной.
Пол
93

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

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

Я бы реализовал это следующим образом:

// 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);
}

РЕДАКТИРОВАТЬ: Исправлены опечатки в примере кода. Вот что я получаю за то, что не проверил это первым!

РЕДАКТИРОВАТЬ: Повышен nullSafeStringComparator в статический.

Эдди
источник
2
Что касается вложенного «если» ... Я считаю, что вложенное «если» будет менее читабельным для этого случая, поэтому я избегаю этого. Да, иногда будет ненужное сравнение из-за этого. final для параметров не является обязательным, но это хорошая идея.
Эдди
31
Сладкое использование XOR
Джеймс МакМэхон
9
@phihag - я знаю, что прошло более 3 лет, НО ... finalключевое слово не является действительно необходимым (Java-код уже многословен как есть.) Однако он предотвращает повторное использование параметров в качестве локальных переменных (ужасная практика кодирования). наше коллективное понимание программного обеспечения со временем улучшается, мы знаем, что по умолчанию все должно быть окончательным / постоянным / неизменным. Поэтому я предпочитаю получить немного больше многословия при использовании finalв объявлениях параметров (как бы тривиально ни была эта функция), чтобы получить ее inmutability-by-quasi-default.) Затраты на ее усвояемость / ремонтопригодность незначительны в общей схеме вещей.
luis.espinal
24
@ Джеймс МакМэхон Я должен не согласиться. Xor (^) можно просто заменить на неравный (! =). Он даже компилируется в один и тот же байт-код. Использование! = Vs ^ это просто вопрос вкуса и читабельности. Итак, судя по тому, что вы были удивлены, я бы сказал, что он здесь не принадлежит. Используйте xor, когда вы пытаетесь вычислить контрольную сумму. В большинстве других случаев (как этот) давайте придерживаться! =.
bvdb
5
Этот ответ легко расширить, заменив String на T, T, объявленный как <T extends Comparable <T >> ... ... и тогда мы сможем безопасно сравнивать любые обнуляемые Comparable объекты
Тьерри
20

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


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

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);
}

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

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

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

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

Редактировать (2009): версия Apache Commons Collections

Собственно, вот способ упростить решение на основе 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. (Как все согласны .)

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

Jonik
источник
2
String.CASE_INSENSITIVE_ORDER действительно делает решение намного чище. Хорошее обновление.
Патрик
2
В любом случае, если вы используете Apache Commons, ComparatorChainвам не нужен собственный compareToметод.
amoebe,
13

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

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

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

Что-то вроде

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);
}

}

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

Патрик
источник
Спасибо, я надеялся, что в Commons будет что-то подобное! В этом случае, однако, я не стал использовать его: stackoverflow.com/questions/481813/…
Jonik
Только что понял, что ваш подход можно упростить с помощью String.CASE_INSENSITIVE_ORDER; см. мой отредактированный последующий ответ.
Jonik
Это хорошо, но проверки "if (other == null) {" не должно быть. В Javadoc для Comparable говорится, что compareTo должен выдавать исключение NullPointerException, если другое равно null.
Даниэль Алексюк
7

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

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

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

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

Петр Собчик
источник
4

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

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); 

}

Йони Ройт
источник
2
Разве «0: 1» не должно быть «0: -1»?
Рольф Кристенсен
3

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

Фабиан Стиг
источник
Да, это, как правило, хорошее решение, которое упрощает многие вещи - но здесь меня больше интересовал случай, когда по той или иной причине допустимы нулевые значения, и это нужно учитывать :)
Jonik
2

Я искал что-то подобное, и это казалось немного сложным, поэтому я сделал это. Я думаю, что это немного легче понять. Вы можете использовать его как компаратор или как один вкладыш. Для этого вопроса вы бы изменили на 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);
    }
}
Dustin
источник
2

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

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
Лев Нг
источник
1

В случае, если кто-то использует 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

Бьерн Бергенхайм
источник
1

Для конкретного случая, когда вы знаете, что данные не будут иметь нулевых значений (это всегда хорошая идея для строк), а данные действительно большие, вы все равно проводите три сравнения перед тем, как сравнивать значения, если вы точно знаете, что это ваш случай , Вы можете оптимизировать немного. 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);
kisna
источник
1
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]
Нихил Кумар К
источник
1

Одним из простых способов использования NullSafe Comparator является использование его реализации в Spring, ниже приведен один из простых примеров для ссылки:

public int compare(Object o1, Object o2) {
        ValidationMessage m1 = (ValidationMessage) o1;
        ValidationMessage m2 = (ValidationMessage) o2;
        int c;
        if (m1.getTimestamp() == m2.getTimestamp()) {
            c = NullSafeComparator.NULLS_HIGH.compare(m1.getProperty(), m2.getProperty());
            if (c == 0) {
                c = m1.getSeverity().compareTo(m2.getSeverity());
                if (c == 0) {
                    c = m1.getMessage().compareTo(m2.getMessage());
                }
            }
        }
        else {
            c = (m1.getTimestamp() > m2.getTimestamp()) ? -1 : 1;
        }
        return c;
    }
Амандип Сингх
источник
0

Еще один пример 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());
}
snp0k
источник
0

Это моя реализация, которую я использую для сортировки моего 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;
}
Ангел Ко
источник
0

Если вы хотите простой Hack:

arrlist.sort((o1, o2) -> {
    if (o1.getName() == null) o1.setName("");
    if (o2.getName() == null) o2.setName("");

    return o1.getName().compareTo(o2.getName());
})

если вы хотите поставить нулевые значения в конец списка, просто измените этот метод выше

return o2.getName().compareTo(o1.getName());
MarsPeople
источник