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

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

// 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 one и 2013 Guava one, оба опубликованные мной,-которые я предпочел в какой-то момент времени.

Я сейчас сделал чистый!--18-->Java 8 Решение от Lukasz Wiktor принято отвечать. Это определенно должно быть предпочтительным, если на Java 8, и в эти дни Java 8 должен быть доступен почти для всех проектов.

16 126

16 ответов:

С помощью 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);
}

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

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

изменить: исправлены опечатки в примере кода. Вот за что я получаю не сначала протестируйте его!

EDIT: повышен 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);
}

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

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

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

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

Edit (2009): Apache Commons Collections version

на самом деле, вот способ сделать решение на основе 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, я бы (конечно) использовал гуавы. (И совершенно определенно не 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), использование nullsLast() будучи единственной гуава-специфической вещью. Эта версия предпочтительнее просто потому, что гуава предпочтительнее, как зависимость, для коллекций Commons. (Как все согласны.)

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

Я всегда рекомендую использовать 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);
}

}

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

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

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

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

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

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

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

}

вы можете создать свой класс, чтобы быть неизменяемым (эффективный 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 для выполнения null-дружественного сравнения между объектами. предположим, у меня есть класс мальчика с 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

в случае, если кто-то использует Spring, есть класс org.springframework.утиль.компаратор.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]

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

С помощью 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));
    }
}

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

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

метод "compareIfNull ()" используется для безопасной сортировки null. Другие методы предназначены для полноты, показывая, как 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);