Как упростить нуль-безопасную реализацию compareTo ()? - PullRequest
135 голосов
/ 27 января 2009

Я реализую 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).

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

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

Ответы [ 16 ]

189 голосов
/ 05 апреля 2012

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

result = ObjectUtils.compare(firstComparable, secondComparable)
147 голосов
/ 28 мая 2014

Использование 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);
}
90 голосов
/ 27 января 2009

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

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

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

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

21 голосов
/ 01 февраля 2009

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

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

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

Edit (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, я бы (конечно) использовал Гуава . (И, конечно, не 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(). Прочитайте Заказ объяснил для более!

13 голосов
/ 27 января 2009

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

Интересующий вас класс - 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);
}

}

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

7 голосов
/ 24 июля 2013

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

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

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

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

4 голосов
/ 27 января 2009

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

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

}

3 голосов
/ 27 января 2009

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

2 голосов
/ 18 марта 2016

мы можем использовать 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
2 голосов
/ 07 октября 2014

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