Как упростить реализацию метода CompareTo(), безопасную для нулевых значений?
-
20-08-2019 - |
Вопрос
я реализую 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)
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);