Як спростити реалізацію нуля безпечного порівняння до ()?


156

Я реалізую 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 1 та Guava 2013, обидва розміщені мною - які я вважав за краще в якийсь момент часу.

Зараз я зробив чисте рішення Java 8 Лукашем Віктором прийнятою відповіддю. Це, безумовно, слід віддати перевагу, якщо на Java 8, а в наші дні Java 8 має бути доступною майже для всіх проектів.


Відповіді:


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 () на об’єкт null. Схоже, проблема з рамками колекцій є чесною, намагаючись розібратися, як це зробити.
Педро Борхес

3
@PedroBorges Автор запитав про сортування об'єктів контейнерів, у яких є сортовані поля (де ці поля можуть бути нульовими), а не про сортування нульових посилань контейнерів. Отже, хоча ваш коментар правильний, оскільки Collections.sort(List)він не працює, коли Список містить нулі, коментар не стосується питання.
Scrubbie

211

Ви можете просто використовувати Apache Commons Lang :

result = ObjectUtils.compare(firstComparable, secondComparable)

4
(@Kong: Це дбає про безпеку нуля, але не нечутливість до випадків, що було ще одним аспектом оригінального питання. Таким чином, не змінюючи прийнятої відповіді.)
Jonik

3
Крім того, на мій погляд, Apache Commons не повинен бути прийнятою відповіддю у 2013 році (Навіть якщо деякі підпроекти краще підтримуються, ніж інші.) Гуаву можна використовувати для досягнення того ж самого ; див. nullsFirst()/ nullsLast().
Джонік

8
@Jonik Чому ви вважаєте, що Apache Commons не повинен бути прийнятою відповіддю у 2013 році?
realnice

1
Велика частина Apache Commons - це застарілі / погано доглянуті / неякісні речі. Для більшості речей, які він пропонує, є кращі альтернативи, наприклад, у Guava, яка є дуже високоякісною лійкою у всьому світі, і все частіше в самому JDK. Близько 2005 року так, Apache Commons було лайно, але в наші дні в більшості проектів цього немає. (Зрозуміло, є винятки; наприклад, якщо мені знадобився FTP-клієнт з якихось причин, я, мабуть, використовую його в Apache Commons Net тощо).
Jonik

6
@Jonik, як би ти відповів на питання, використовуючи Guava? Ваше твердження, що 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);
}

EDIT: Виправлені помилки друку у зразку коду. Ось що я отримую за те, щоб не перевірити його спочатку!

EDIT: Підвищено nullSafeStringComparator до статики.


2
Щодо вкладеного "якщо" ... я вважаю вкладене, якщо воно менш читабельним для цього випадку, тому я уникаю цього. Так, інколи через це буде непотрібне порівняння. остаточний для параметрів не потрібен, але це гарна ідея.
Едді

31
Солодке використання XOR
James McMahon

9
@phihag - Я знаю, що це вже понад 3 роки, АЛЕ ... finalключове слово справді не потрібне (код Java вже є багатослівним, як є.) Однак він перешкоджає повторному використанню параметрів як локальних vars (жахлива практика кодування.) наше колективне розуміння програмного забезпечення з часом покращується, ми знаємо, що за замовчуванням все повинно бути остаточним / const / незмінним. Тому я віддаю перевагу дещо додатковому багатослівному використанню finalв деклараціях параметрів (як би тривіальна функція не була), щоб отримати inmutability-by-quasi-default.) Потужність зрозумілості / ремонтопридатності є незначною у великій схемі речей.
luis.espinal

24
@James McMahon Я не погоджуюся. Xor (^) можна було просто замінити на не рівний (! =). Він навіть компілює в один і той же байт-код. Використання! = Vs ^ - лише питання смаку та читабельності. Отже, судячи з того, що ви були здивовані, я б сказав, що це не належить тут. Використовуйте xor, коли ви намагаєтеся розрахувати контрольну суму. У більшості інших випадків (як цей) дотримуймося! =.
bvdb

5
Цю відповідь легко розширити, замінивши String на T, T, оголошений як <T, порівнює <T>> ..., і тоді ми можемо сміливо порівнювати будь-які нульові порівнянні об'єкти
Тьєррі

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

Ось так визначається помічник (він перевантажений, так що ви також можете визначити, чи нулі приходять першими чи останніми, якщо хочете):

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

Отже, це по суті те саме , що відповідь Едді (хоча я не назвав би статичний помічник методом порівняння ) і відповідь ужина теж.

У будь-якому разі, загалом, я б наголосив на рішенні Патріка , оскільки, на мою думку, це вдала практика використовувати створені бібліотеки по можливості. ( Знай і використовуй бібліотеки, як каже Джош Блох.) Але в цьому випадку це не дало б найчистішого, найпростішого коду.

Редагувати (2009): Версія колекцій Apache Commons

Насправді, ось спосіб зробити рішення на основі 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);
}

Зараз це досить елегантно, я думаю. (Залишилося лише одне невелике питання: The 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 (обидва використовують CASE_INSENSITIVE_ORDER JDK ), nullsLast()оскільки це є єдиною річчю, характерною для Guava. Ця версія є кращою лише тому, що Guava є кращим, як залежність, від Commons Collection. (Як всі згодні .)

Якщо вам було цікаво Ordering, зверніть увагу, що він реалізує Comparator. Це досить зручно, особливо для більш складних потреб у сортуванні, що дозволяє, наприклад, ланцюжком використовувати декілька замовлень compound(). Читайте замовлення, пояснивши докладніше!


2
String.CASE_INSENSITIVE_ORDER дійсно робить рішення набагато чистішим. Приємне оновлення.
Патрік

2
Якщо ви все-таки використовуєте Apache Commons, є ComparatorChainтакий, що вам не потрібен власний compareToметод.
амебе

13

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

Клас, який вас цікавить, - це Null Comparator . Це дозволяє зробити нулі високими або низькими. Ви також даєте йому власний компаратор для використання, коли два значення не є нульовими.

У вашому випадку ви можете мати статичну змінну члена, яка робить порівняння, а потім ваш 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);
}

}

Навіть якщо ви вирішили зробити свій власний, пам’ятайте про цей клас, оскільки він дуже корисний при замовленні списків, що містять нульові елементи.


Дякую, я якось сподівався, що щось подібне стане у Вікісховищі! Однак у цьому випадку я не закінчив його використовувати: stackoverflow.com/questions/481813/…
Jonik

Щойно зрозумів, що ваш підхід можна спростити за допомогою String.CASE_INSENSITIVE_ORDER; дивіться мою відредаговану відповідь.
Джонік

Це добре, але чек "if (other == null) {" не повинен бути там. Javadoc for Comprable говорить, що CompareTo повинен кинути NullPointerException, якщо інше недійсне.
Даніель Алексюк

7

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

Але я просто хочу зазначити, що підтримка нульових значень у порівнянніТож не узгоджується з контрактом порівнянняКомпанії, описаного в офіційному javadocs для Порівняння :

Зауважте, що null не є примірником будь-якого класу, а e.compareTo (null) повинен кидати NullPointerException, навіть якщо e.equals (null) повертає помилково.

Тож я б або кинув 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"?
Rolf Kristensen

3

Ви можете розробити свій клас непорушним (Ефективна Java 2nd Ed. Має чудовий розділ про це, Пункт 15: Мінімізація змінності) та переконайтесь, що після побудови немає можливих нулей (і використовуйте нульовий шаблон об'єкта, якщо це потрібно). Тоді ви можете пропустити всі ці перевірки та безпечно припустити, що значення не є нульовими.


Так, це, як правило, хороше рішення і спрощує багато речей - але тут мене більше зацікавив випадок, коли нульові значення дозволені з тих чи інших причин, і їх потрібно враховувати :)
Jonik

2

Я шукав щось подібне, і це здавалося трохи складним, тому я це зробив. Я думаю, що це трохи простіше зрозуміти. Ви можете використовувати його як компаратор або як один вкладиш. Для цього питання ви б змінили порівнятиToIgnoreCase (). Як і, нулі спливають. Ви можете перевернути 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);
    }
}

2

ми можемо використовувати java 8, щоб зробити нульову порівняння між об'єктом. гадаю, я хава клас хлопчика з 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

У конкретному випадку, коли ви знаєте, що дані не матимуть нулів (завжди хороша ідея для рядків), а дані дійсно великі, ви все ще робите три порівняння, перш ніж насправді порівнювати значення, якщо ви точно знаєте, що це ваш випадок , Ви можете оптимізувати біт TAD. 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);

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 - використання весняної реалізації його, нижче - один з простих прикладів для посилання:

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

0

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

у моєму випадку EntityPhone розширює EntityAb Abstract, а мій контейнер - Список <EntityAbrief>.

метод "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

Якщо ви хочете просту хаку:

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());
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.