Как упростить реализацию метода CompareTo(), безопасную для нулевых значений?

StackOverflow https://stackoverflow.com/questions/481813

Вопрос

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

Это было полезно?

Решение

С использованием Ява 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);
}

Другие советы

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

result = ObjectUtils.compare(firstComparable, secondComparable)

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

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

Я бы реализовал это примерно так:

// 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 повышено до статического.

См. нижнюю часть этого ответа для обновленного (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

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

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

Обновление (2013 г.):Версия Гуавы

Почти 5 лет спустя, вот как я бы ответил на свой первоначальный вопрос.Если бы я писал код на Java, я бы (конечно) использовал Гуава.(И совершенно точно нет 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 в качестве зависимости предпочтительнее Commons Collections.(Как все согласны.)

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

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

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

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

}

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

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

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

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

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

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

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

}

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

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

мы можем использовать Java 8 для сравнения объектов между объектами, дружественного к нулю.Предположим, у меня есть класс 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

Если кто-то использует 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

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]

Один из простых способов использование компаратора NullSafe заключается в использовании его реализации 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;
    }

Еще один пример 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());
}

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

В конкретном случае, когда вы знаете, что данные не будут иметь нулевых значений (всегда хорошая идея для строк) и данные действительно большие, вы все равно выполняете три сравнения, прежде чем фактически сравнивать значения: если ты точно знаешь, что это твой случай, вы можете немного оптимизировать.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);
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top