Интерфейсы Comparable и Comparator
Для того чтобы объекты можно было сравнить и сортировать, они должны реализовать параметризованный интерфейс Comparable .
Интерфейс Comparable содержит один единственный метод int compareTo(T item) , который сравнивает текущий объект с объектом, переданным в качестве параметра.
Если этот метод возвращает отрицательное число, то текущий объект будет располагаться перед тем, который передается через параметр. Если метод вернет положительное число, то, наоборот, после второго объекта. Если метод возвращает ноль, значит, оба объекта равны.
Рассмотрим пример реализации интерфейса Comparable :
public class Person implements Comparable < private String firstName; private String lastName; private int age; public Person(String firstName, String lastName, int age) < this.firstName = firstName; this.lastName = lastName; this.age = age; >public String getFirstName() < return firstName; >public void setFirstName(String firstName) < this.firstName = firstName; >public String getLastName() < return lastName; >public void setLastName(String lastName) < this.lastName = lastName; >public int getAge() < return age; >public void setAge(int age) < this.age = age; >@Override public int compareTo(Person anotherPerson) < int anotherPersonAge = anotherPerson.getAge(); return this.age - anotherPersonAge; >@Override public String toString() < return "Personpublic class ComparePersonDemo < public static void main(String[] args) < SortedSetset = new TreeSet<>(); set.add(new Person("Саша", "Иванов", 36)); set.add(new Person("Маша", "Петрова", 23)); set.add(new Person("Даша", "Сидорова", 34)); set.add(new Person("Вася", "Иванов", 25)); set.forEach(System.out::println); > >
2. Интерфейс Comparator
Если класс по какой-то причине не может реализовать интерфейс Comparable , или же просто нужен другой вариант сравнения, используется интерфейс Comparator .
Интерфейс содержит метод int compare(T o1, T o2) , который должен быть реализован классом, реализующим компаратор.
Метод compare возвращает числовое значение — если оно отрицательное, то объект o1 предшествует объекту o2 , иначе — наоборот. А если метод возвращает ноль, то объекты равны.
Для применения интерфейса нам вначале надо создать класс компаратора, который реализует этот параметризованный интерфейс.
Рассмотрим пример использования интерфейса Comparator :
import java.util.Comparator; public class PersonComparator implements Comparator < @Override public int compare(Person o1, Person o2) < return o1.getLastName().compareTo(o2.getLastName()); >>
import java.util.SortedSet; import java.util.TreeSet; public class PersonComparatorDemo < public static void main(String[] args) < PersonComparator personComparator = new PersonComparator(); SortedSetset = new TreeSet<>(personComparator); set.add(new Person("Саша", "Иванов", 36)); set.add(new Person("Маша", "Петрова", 23)); set.add(new Person("Даша", "Сидорова", 34)); set.add(new Person("Вася", "Иванов", 25)); //Обратите внимание - было добавлено 4 элемента, но распечатано 3 set.forEach(System.out::println); > >
То же самое перепишем с использованием метода comparing() интерфейса Comparator :
import java.util.Comparator; import java.util.SortedSet; import java.util.TreeSet; public class PersonComparingDemo < public static void main(String[] args) < ComparatorpersonComparator = Comparator.comparing(Person::getLastName).thenComparing(Person::getAge); SortedSet set = new TreeSet<>(personComparator); set.add(new Person("Саша", "Иванов", 36)); set.add(new Person("Маша", "Петрова", 23)); set.add(new Person("Даша", "Сидорова", 34)); set.add(new Person("Вася", "Иванов", 25)); set.forEach(System.out::println); > >
- Интерфейс Collection
- Структуры данных
- Интерфейс List и класс ArrayList
- Интерфейс Set и классы HashSet, LinkedHashSet
- Интерфейс SortedSet и класс TreeSet
- Интерфейс NavigableSet
- Интерфейсы Queue и Deque
- Класс ArrayDeque
- Класс LinkedList
- Класс PriorityQueue
- Принцип PECS
- Интерфейс Iterator
- Интерфейс ListIterator
- Отображения Map
- Класс Collections
- Backed Collections
- Legacy Classes
- Задания
Компаратор java что это
В прошлой теме была рассмотрена работа коллекции TreeSet, типизированной объектами String. При добавлении новых элементов объект TreeSet автоматически проводит сортировку, помещая новый объект на правильное для него место. Однако со строками все понятно. А что если бы мы использовали не строки, а свои классы, например, следующий класс Person:
class Person < private String name; Person(String name)< this.name=name; >String getName() >
Объект TreeSet мы не сможем типизировать данным классом, поскольку в случае добавления объектов TreeSet не будет знать, как их сравнивать, и следующий кусок кода не будет работать:
TreeSet people = new TreeSet(); people.add(new Person("Tom"));
При выполнении этого кода мы столкнемся с ошибкой, которая скажет, что объект Person не может быть преобразован к типу java.lang.Comparable.
Для того, чтобы объекты Person можно было сравнить и сортировать, они должны применять интерфейс Comparable . При применении интерфейса он типизируется текущим классом. Применим его к классу Person:
class Person implements Comparable < private String name; Person(String name)< this.name = name; >String getName() public int compareTo(Person p) < return name.compareTo(p.getName()); >>
Интерфейс Comparable содержит один единственный метод int compareTo(E item) , который сравнивает текущий объект с объектом, переданным в качестве параметра. Если этот метод возвращает отрицательное число, то текущий объект будет располагаться перед тем, который передается через параметр. Если метод вернет положительное число, то, наоборот, после второго объекта. Если метод возвратит ноль, значит, оба объекта равны.
В данном случае мы не возвращаем явным образом никакое число, а полагаемся на встроенный механизм сравнения, который есть у класса String. Но мы также можем определить и свою логику, например, сравнивать по длине имени:
public int compareTo(Person p)
Теперь мы можем типизировать TreeSet типом Person и добавлять в дерево соответствующие объекты:
TreeSet people = new TreeSet(); people.add(new Person("Tom"));
Интерфейс Comparator
Однако перед нами может возникнуть проблема, что если разработчик не реализовал в своем классе, который мы хотим использовать, интерфейс Comparable, либо реализовал, но нас не устраивает его функциональность, и мы хотим ее переопределить? На этот случай есть еще более гибкий способ, предполагающий применение интерфейса Comparator.
Интерфейс Comparator содержит ряд методов, ключевым из которых является метод compare() :
public interface Comparator < int compare(T a, T b); // остальные методы >
Метод compare также возвращает числовое значение — если оно отрицательное, то объект a предшествует объекту b, иначе — наоборот. А если метод возвращает ноль, то объекты равны. Для применения интерфейса нам вначале надо создать класс компаратора, который реализует этот интерфейс:
class PersonComparator implements Comparator < public int compare(Person a, Person b)< return a.getName().compareTo(b.getName()); >>
Здесь опять же проводим сравнение по строкам. Теперь используем класс компаратора для создания объекта TreeSet:
PersonComparator pcomp = new PersonComparator(); TreeSet people = new TreeSet(pcomp); people.add(new Person(«Tom»)); people.add(new Person(«Nick»)); people.add(new Person(«Alice»)); people.add(new Person(«Bill»)); for(Person p : people)
Для создания TreeSet здесь используется одна из версий конструктора, которая в качестве параметра принимает компаратор. Теперь вне зависимости от того, реализован ли в классе Person интерфейс Comparable, логика сравнения и сортировки будет использоваться та, которая определена в классе компаратора.
Сортировка по нескольким критериям
Начиная с JDK 8 в механизм работы компараторов были внесены некоторые дополнения. В частности, теперь мы можем применять сразу несколько компараторов по принципу приоритета. Например, изменим класс Person следующим образом:
class Person < private String name; private int age; public Person(String n, int a)< name=n; age=a; >String getName() int getAge() >
Здесь добавлено поле для хранения возраста пользователя. И, допустим, нам надо отсортировать пользователей по имени и по возрасту. Для этого определим два компаратора:
class PersonNameComparator implements Comparator < public int compare(Person a, Person b)< return a.getName().compareTo(b.getName()); >> class PersonAgeComparator implements Comparator < public int compare(Person a, Person b)< if(a.getAge()>b.getAge()) return 1; else if(a.getAge() < b.getAge()) return -1; else return 0; >>
Интерфейс компаратора определяет специальный метод по умолчанию thenComparing , который позволяет использовать цепочки компараторов для сортировки набора:
Comparator pcomp = new PersonNameComparator().thenComparing(new PersonAgeComparator()); TreeSet people = new TreeSet(pcomp); people.add(new Person(«Tom», 23)); people.add(new Person(«Nick»,34)); people.add(new Person(«Tom»,10)); people.add(new Person(«Bill»,14)); for(Person p : people)
Bill 14 Nick 34 Tom 10 Tom 23
В данном случае сначала применяется сортировка по имени, а потом по возрасту.
31.20. Java – Comparator
И TreeSet, и TreeMap хранят элементы в отсортированном порядке. Однако именно компаратор в Java точно определяет, что такое отсортированный порядок.
Методы
Интерфейс Comparator в Java определяет два метода: compare() и equals(). Метод compare(), показанный здесь, сравнивает два элемента:
Метод Compare
int compare(Object obj1, Object obj2)
obj1 и obj2 — объекты, которые нужно сравнить. Этот метод возвращает ноль, если объекты равны. Он возвращает положительное значение, если obj1 больше, чем obj2. В противном случае возвращается отрицательное значение. При переопределении compare() вы можете изменить способ упорядочивания объектов. Например, для сортировки в обратном порядке вы можете создать компаратор, который изменит исход сравнения.
Метод Equals
Метод equals(), показанный здесь, проверяет, равен ли объект вызывающему компаратору.
boolean equals(Object obj)
obj — объект, подлежащий проверке на равенство. Метод возвращает true, если obj и вызывающий объект являются объектами Comparator и используют один и тот же порядок. В противном случае возвращается false.
Переопределять equals () не нужно, и большинство простых компараторов этого не сделают.
Пример
import java.util.*; class Dog implements Comparator, Comparable < private String name; private int age; Dog() < >Dog(String n, int a) < name = n; age = a; >public String getDogName() < return name; >public int getDogAge() < return age; >// Переопределяем метод compareTo public int compareTo(Dog d) < return (this.name).compareTo(d.name); >// Переопределяем метод compare для сортировки возраста public int compare(Dog d, Dog d1) < return d.age - d1.age; >> public class Example < public static void main(String args[]) < // Берём список элементов Dog Listlist = new ArrayList(); list.add(new Dog("Шегги", 3)); list.add(new Dog("Лэси", 2)); list.add(new Dog("Роджер", 10)); list.add(new Dog("Томми", 4)); list.add(new Dog("Тамми", 1)); Collections.sort(list); // Сортируем список массивов for(Dog a: list) // Выводим сортированный список имён System.out.print(a.getDogName() + ", "); // Сортируем список массивов, используя компаратор Collections.sort(list, new Dog()); System.out.println(" "); for(Dog a: list) // Выводим сортированный список возрастов System.out.print(a.getDogName() +" : "+ a.getDogAge() + ", "); > >
Получим следующий результат:
Лэси, Роджер, Шэгги, Тамми, Томми, Тамми : 1, Лэси : 2, Лэси : 3, Томми : 4, Роджер : 10,
Примечание: сортировка класса Arrays аналогична классу Collections.
Оглавление
- 1. Java – Самоучитель для начинающих
- 2. Java – Обзор языка
- 3. Java – Установка и настройка
- 4. Java – Синтаксис
- 5. Java – Классы и объекты
- 6. Java – Конструкторы
- 7. Java – Типы данных и литералы
- 8. Java – Типы переменных
- 9. Java – Модификаторы
- 10. Java – Операторы
- 11. Java – Циклы и операторы цикла
- 11.1. Java – Цикл while
- 11.2. Java – Цикл for
- 11.3. Java – Улучшенный цикл for
- 11.4. Java – Цикл do..while
- 11.5. Java – Оператор break
- 11.6. Java – Оператор continue
- 12. Java – Операторы принятия решений
- 12.1. Java – Оператор if
- 12.2. Java – Оператор if..else
- 12.3. Java – Вложенный оператор if
- 12.4. Java – Оператор switch..case
- 12.5. Java – Условный оператор (? 🙂
- 13. Java – Числа
- 13.1. Java – Методы byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue()
- 13.2. Java – Метод compareTo()
- 13.3. Java – Метод equals()
- 13.4. Java – Метод valueOf()
- 13.5. Java – Метод toString()
- 13.6. Java – Метод parseInt()
- 13.7. Java – Метод Math.abs()
- 13.8. Java – Метод Math.ceil()
- 13.9. Java – Метод Math.floor()
- 13.10. Java – Метод Math.rint()
- 13.11. Java – Метод Math.round()
- 13.12. Java – Метод Math.min()
- 13.13. Java – Метод Math.max()
- 13.14. Java – Метод Math.exp()
- 13.15. Java – Метод Math.log()
- 13.16. Java – Метод Math.pow()
- 13.17. Java – Метод Math.sqrt()
- 13.18. Java – Метод Math.sin()
- 13.19. Java – Метод Math.cos()
- 13.20. Java – Метод Math.tan()
- 13.21. Java – Метод Math.asin()
- 13.22. Java – Метод Math.acos()
- 13.23. Java – Метод Math.atan()
- 13.24. Java – Метод Math.atan2()
- 13.25. Java – Метод Math.toDegrees()
- 13.26. Java – Метод Math.toRadians()
- 13.27. Java – Метод Math.random()
- 14. Java – Символы
- 14.1. Java – Метод Character.isLetter()
- 14.2. Java – Метод Character.isDigit()
- 14.3. Java – Метод Character.isWhitespace()
- 14.4. Java – Метод Character.isUpperCase()
- 14.5. Java – Метод Character.isLowerCase()
- 14.6. Java – Метод Character.toUpperCase()
- 14.7. Java – Метод Character.toLowerCase()
- 14.8. Java – Метод Character.toString()
- 15. Java – Строки
- 15.1. Java – Метод charAt()
- 15.2. Java – Метод compareTo()
- 15.3. Java – Метод compareToIgnoreCase()
- 15.4. Java – Метод concat()
- 15.5. Java – Метод contentEquals()
- 15.6. Java – Метод copyValueOf()
- 15.7. Java – Метод endsWith()
- 15.8. Java – Метод equals()
- 15.9. Java – Метод equalsIgnoreCase()
- 15.10. Java – Метод getBytes()
- 15.11. Java – Метод getChars()
- 15.12. Java – Метод hashCode()
- 15.13. Java – Метод indexOf()
- 15.14. Java – Метод intern()
- 15.15. Java – Метод lastIndexOf()
- 15.16. Java – Метод length()
- 15.17. Java – Метод matches()
- 15.18. Java – Метод regionMatches()
- 15.19. Java – Метод replace()
- 15.20. Java – Метод replaceAll()
- 15.21. Java – Метод replaceFirst()
- 15.22. Java – Метод split()
- 15.23. Java – Метод startsWith()
- 15.24. Java – Метод subSequence()
- 15.25. Java – Метод substring()
- 15.26. Java – Метод toCharArray()
- 15.27. Java – Метод toLowerCase()
- 15.28. Java – Метод toString()
- 15.29. Java – Метод toUpperCase()
- 15.30. Java – Метод trim()
- 15.31. Java – Метод valueOf()
- 15.32. Java – Классы StringBuilder и StringBuffer
- 15.32.1. Java – Метод append()
- 15.32.2. Java – Метод reverse()
- 15.32.3. Java – Метод delete()
- 15.32.4. Java – Метод insert()
- 15.32.5. Java – Метод replace()
- 16. Java – Массивы
- 17. Java – Дата и время
- 18. Java – Регулярные выражения
- 19. Java – Методы
- 20. Java – Потоки ввода/вывода, файлы и каталоги
- 20.1. Java – Класс ByteArrayInputStream
- 20.2. Java – Класс DataInputStream
- 20.3. Java – Класс ByteArrayOutputStream
- 20.4. Java – Класс DataOutputStream
- 20.5. Java – Класс File
- 20.6. Java – Класс FileReader
- 20.7. Java – Класс FileWriter
- 21. Java – Исключения
- 21.1. Java – Встроенные исключения
- 22. Java – Вложенные и внутренние классы
- 23. Java – Наследование
- 24. Java – Переопределение
- 25. Java – Полиморфизм
- 26. Java – Абстракция
- 27. Java – Инкапсуляция
- 28. Java – Интерфейсы
- 29. Java – Пакеты
- 30. Java – Структуры данных
- 30.1. Java – Интерфейс Enumeration
- 30.2. Java – Класс BitSet
- 30.3. Java – Класс Vector
- 30.4. Java – Класс Stack
- 30.5. Java – Класс Dictionary
- 30.6. Java – Класс Hashtable
- 30.7. Java – Класс Properties
- 31. Java – Коллекции
- 31.1. Java – Интерфейс Collection
- 31.2. Java – Интерфейс List
- 31.3. Java – Интерфейс Set
- 31.4. Java – Интерфейс SortedSet
- 31.5. Java – Интерфейс Map
- 31.6. Java – Интерфейс Map.Entry
- 31.7. Java – Интерфейс SortedMap
- 31.8. Java – Класс LinkedList
- 31.9. Java – Класс ArrayList
- 31.10. Java – Класс HashSet
- 31.11. Java – Класс LinkedHashSet
- 31.12. Java – Класс TreeSet
- 31.13. Java – Класс HashMap
- 31.14. Java – Класс TreeMap
- 31.15. Java – Класс WeakHashMap
- 31.16. Java – Класс LinkedHashMap
- 31.17. Java – Класс IdentityHashMap
- 31.18. Java – Алгоритмы Collection
- 31.19. Java – Iterator и ListIterator
- 31.20. Java – Comparator
- 32. Java – Дженерики
- 33. Java – Сериализация
- 34. Java – Сеть
- 34.1. Java – Обработка URL
- 35. Java – Отправка Email
- 36. Java – Многопоточность
- 36.1. Java – Синхронизация потоков
- 36.2. Java – Межпоточная связь
- 36.3. Java – Взаимная блокировка потоков
- 36.4. Java – Управление потоками
- 37. Java – Основы работы с апплетами
- 38. Java – Javadoc
Пишите компараторы правильно
В Java для введения порядка среди определённых объектов можно написать компаратор — класс, содержащий функцию compare , которая сравнивает два объекта. Альтернативой компаратору является естественный порядок объектов: объект реализует интерфейс Comparable , который содержит метод compareTo , позволяющий сравнить этот объект с другим. Сравнивающая функция должна вернуть 0, если объекты равны, отрицательное число (обычно -1), если первый объект меньше второго, и положительное число (обычно 1), если первый больше. Обычно реализация такой функции не представляет сложностей, но имеется один случай, о котором многие забывают.
Сравнение используется различными алгоритмами от сортировки и двоичного поиска до поддержания порядка в сортированных коллекциях вроде TreeMap . Эти алгоритмы завязаны на три важных свойства сравнивающей функции: рефлексивность (сравнение элемента с самим собой всегда даёт 0), антисимметричность (сравнение A с B и B с A должны дать разный знак) и транзитивность (если сравнение A с B и B с C выдаёт одинаковый знак, то и сравнение A с C должно выдать такой же). Если сравнивающая функция не удовлетворяет этим свойствам, алгоритм может выдать совершенно непредсказуемый результат. Причём скорее всего вы не получите никакого исключения, просто результат будет неверный.
Как обнаружилось, несоблюдение этих свойств — не такая уж редкая ситуация. Проблема возникает при сравнении вещественных чисел — float или double.
Предположим, у нас имеется класс с полем типа double, и мы хотим упорядочивать объекты этого класса в зависимости от значения поля. Нередко можно встретить такой код:
public class DoubleHolder implements Comparable < double d; public DoubleHolder(double d) < this.d = d; >@Override public int compareTo(DoubleHolder o) < return d >o.d ? 1 : d == o.d ? 0 : -1; > @Override public String toString() < return String.valueOf(d); >>
У приведённого метода compareTo есть две проблемы. Первая — незначительная — он не различает +0.0 и -0.0: new DoubleHolder(-0.0).compareTo(new DoubleHolder(+0.0)) вернёт 0. Иногда это нестрашно, но в случае сортировки элементы с +0.0 и -0.0 расположатся в произвольном порядке, что будет смотреться некрасиво. Тем не менее, всё это мелочи по сравнению с NaN. Число NaN (как в типе double, так и во float) — это довольно специфичная вещь. Оно не больше, не меньше и не равно никакому другому числу. В результате мы сразу получаем нарушение свойств сравнения:
DoubleHolder nan = new DoubleHolder(Double.NaN); DoubleHolder zero = new DoubleHolder(0.0); System.out.println("nan.compareTo(nan): "+nan.compareTo(nan)); System.out.println("nan.compareTo(zero): "+nan.compareTo(zero)); System.out.println("zero.compareTo(nan): "+zero.compareTo(nan));
nan.compareTo(nan): -1 nan.compareTo(zero): -1 zero.compareTo(nan): -1
Ни рефлексивности, ни антисимметричности не наблюдается. Можно встретить и такую реализацию сравнения:
public int compareTo(DoubleHolder o) < return d >o.d ? 1 : d
Здесь все три предыдущих сравнения выдадут ноль, то есть как будто бы свойства соблюдаются. Но, конечно, радоваться рано:
DoubleHolder nan = new DoubleHolder(Double.NaN); DoubleHolder zero = new DoubleHolder(0.0); DoubleHolder one = new DoubleHolder(1.0); System.out.println("zero.compareTo(nan): "+zero.compareTo(nan)); System.out.println("nan.compareTo(one): "+nan.compareTo(one)); System.out.println("zero.compareTo(one): "+zero.compareTo(one));
zero.compareTo(nan): 0 nan.compareTo(one): 0 zero.compareTo(one): -1
Здесь нарушается транзитивность: первый объект равен второму, второй равен третьему, но первый третьему не равен.
Чем же это грозит простому обывателю? Чтобы понять это, создадим простой список и попробуем его перемешать и посортировать несколько раз:
List data = new ArrayList<>(); for(int i=1; i data.add(new DoubleHolder(Double.NaN)); for(int i=0; i
Вывод в каждом запуске отличается и может выглядеть, например, так:
[NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [1.0, NaN, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN] [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, NaN, 1.0, 9.0, 10.0] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, NaN, 9.0] [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [1.0, NaN, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
Или для второй реализации compareTo :
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 9.0, NaN, 7.0, 10.0] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN] [1.0, 2.0, 9.0, 10.0, NaN, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN] [2.0, 6.0, NaN, 1.0, 3.0, 4.0, 5.0, 7.0, 8.0, 9.0, 10.0] [2.0, 4.0, NaN, 1.0, 3.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [1.0, 3.0, NaN, 2.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, NaN]
Примерно в половине случаев элемент с NaN прибивается к началу или концу сортируемого списка, а в других случаях начинает блуждать, портя порядок окружающих элементов.
С коллекциями, использующими DoubleHolder в качестве ключа, тоже ничего хорошего не произойдёт. Возьмём, к примеру, TreeSet . Со второй реализацией compareTo всё довольно просто: так как элемент, содержащий NaN, равен любому другому, то в непустое множество вставить его не получится, потому что оно решит, что такой элемент уже есть. Но берегитесь, если вы вставили NaN-элемент первым: после этого во множество не выйдет добавить ничего другого.
Первый вариант сравнивающей функции психоделичнее. Напишем, например, такой тест:
Set set = new TreeSet<>(); for(int i=0; i System.out.println(set);
Мы вставили по пять элементов, содержащих NaN, и по пять элементов, содержащих каждую цифру от 1 до 9. В результате имеем следующее:
[NaN, NaN, 1.0, 2.0, 3.0, NaN, NaN, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, NaN]
Вполне ожидаемо увидеть пять раз NaN: ведь они не равны друг другу. Но из-за неправильных сравнений и некоторые другие элементы вставились по нескольку раз. Можете сами посмотреть, что случится с TreeMap. Заметьте, что один случайно попавший NaN может испортить всю коллекцию, причём это не всегда легко отладить: коллекция может долго существовать в некорректном состоянии и делать вид, что всё нормально.
Что любопытно, этой проблемы вообще не должно существовать. Ещё в JDK 1.4 появились специальные статические методы Float.compare и Double.compare, которые сделают всё за вас, корректно обработав специальные случаи. Надо лишь написать:
public int compareTo(DoubleHolder o)
Видимо, сказывается обманчивая простота алгоритма сравнения: порой программист считает, что проще написать самому, чем искать в документации стандартный способ сделать это.
Примеры неправильного сравнения double/float в различных открытых проектах: JTS, Batik, Hadoop, Hudson, ICU4J, Lucene. Трудно определить, в каких случаях это может привести к проблемам, но это тот случай, когда я бы исправлял безусловно: правильный и надёжный вариант обычно при этом ещё и короче неправильного.
Чтобы изменить ситуацию, я написал маленький детектор для FindBugs, который находит некорректно реализованные функции сравнения и предлагает использовать Float.compare/Double.compare.
Вообще для всех примитивных типов есть подобные методы. Если вам надо сравнить несколько полей по очереди, можно написать так:
public class MyObject implements Comparable < double d; int i; String s; char c; @Override public int compareTo(MyObject o) < int result; result = Double.compare(d, o.d); if(result != 0) return result; result = Integer.compare(i, o.i); if(result != 0) return result; result = s.compareTo(o.s); if(result != 0) return result; result = Character.compare(c, o.c); return result; >>
Смотрится лучше, чем куча веток с больше и меньше. А если вы пользуетесь Guava или чем-то подобным, тогда так:
@Override public int compareTo(MyObject o)