Comparator.compare: примеры (JAVA)
Comparator.compare(T o1, T o2): intОписание метода Comparator.compare
Метод compare - это абстрактный метод интерфейса java.util.Comparator<T>, задающий правило сравнения двух объектов типа T. Сигнатура выглядит так:
int compare(T o1, T o2)
Когда применяется: при сортировке коллекций и массивов (Collections.sort, Arrays.sort, Stream.sorted), при хранении упорядоченных множеств и карт (TreeSet, TreeMap, PriorityQueue) и в любых алгоритмах, где требуется определить порядок между двумя элементами.
Аргументы:
- o1 - первый объект для сравнения.
- o2 - второй объект для сравнения.
Важно: интерфейс не гарантирует обработку null. Если компаратор не реализует явной логики для null, вызов compare с null может привести к NullPointerException. Для удобства предоставлены утилиты Comparator.nullsFirst и Comparator.nullsLast.
Возвращаемое значение:
- Отрицательное значение < 0 - объект o1 должен располагаться раньше o2.
- Ноль = 0 - объекты считаются равными в смысле порядка (не обязательно equals).
- Положительное значение > 0 - o1 должен располагаться после o2.
Требования к реализации (контракт):
- Антисимметрия: sign(compare(a, b)) == -sign(compare(b, a)).
- Транзитивность: если compare(a, b) > 0 и compare(b, c) > 0, то compare(a, c) > 0.
- Согласованность с equals: если compare(a, b) == 0, желательно, чтобы a.equals(b) было true, особенно если компаратор используется в множестве/карте, где равенство порядка эквивалентно дубликатам.
Практические рекомендации:
- Для числовых типов использовать Integer.compare, Long.compare, Double.compare и т. п., чтобы избежать переполнения при вычитании.
- Использовать Comparator.comparing и его специализированные методы (comparingInt, comparingLong, comparingDouble) для удобства и читаемости.
- Следить за согласованностью компаратора с equals, если объекты будут храниться в TreeSet/TreeMap.
Короткие примеры использования
1. Простой компаратор для целых через lambda и сорировка списка:
import java.util.*;
class Example1 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 1, 10, -2);
list.sort((a, b) -> Integer.compare(a, b));
System.out.println(list);
}
}
[-2, 1, 5, 10]
2. Сравнение строк без учета регистра и обратный порядок:
import java.util.*;
class Example2 {
public static void main(String[] args) {
List<String> names = Arrays.asList("bob", "Alice", "charlie");
names.sort(String.CASE_INSENSITIVE_ORDER.reversed());
System.out.println(names);
}
}
[charlie, bob, Alice]
3. Компаратор для пользовательского класса с использованием Comparator.comparing:
import java.util.*;
class Person { String name; int age; Person(String n,int a){name=n;age=a;} public String toString(){return name+":"+age;} }
class Example3 {
public static void main(String[] args) {
List<Person> ppl = Arrays.asList(new Person("Anna",30), new Person("Ben",25), new Person("Anna",22));
ppl.sort(Comparator.comparing((Person p) -> p.name).thenComparingInt(p -> p.age));
System.out.println(ppl);
}
}
[Anna:22, Anna:30, Ben:25]
4. Обработка null при сравнении:
import java.util.*;
class Example4 {
public static void main(String[] args) {
List<String> list = Arrays.asList("b", null, "a");
list.sort(Comparator.nullsLast(String::compareTo));
System.out.println(list);
}
}
[a, b, null]
5. Comparator как аргумент в TreeSet и PriorityQueue:
import java.util.*;
class Example5 {
public static void main(String[] args) {
Comparator<Integer> cmp = Comparator.reverseOrder();
TreeSet<Integer> ts = new TreeSet<>(cmp);
ts.addAll(Arrays.asList(1,3,2));
System.out.println(ts);
PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);
pq.addAll(Arrays.asList(1,3,2));
System.out.println(pq.poll());
}
}
[3, 2, 1] 3
Похожие механизмы в Java
- Comparable (интерфейс): реализует метод compareTo(T) внутри класса. Подходит, когда класс имеет естественный порядок. Предпочтение - если порядок логично привязан к типу.
- Comparator.comparing и специализированные методы: синтаксический сахар для создания компараторов на основе ключей. Удобнее для сложных цепочек сравнения и при работе с внешними критериями.
- Collator: для локализованного сравнения строк (например, учёт языковых правил). Предпочтительнее, когда порядок должен учитывать локаль.
- Comparator.reversed, thenComparing, nullsFirst/nullsLast: функциональные методы, расширяющие поведение compare. Удобны для комбинирования правил.
- Apache Commons ComparatorChain (внешняя библиотека): серия компараторов с приоритетом. Полезно, если нужна более старшая инфраструктура, но в Java 8+ часто достаточно стандартных утилит.
Аналоги в других языках и отличия
PHP
<?
$arr = [5, 1, 10, -2];
usort($arr, function($a, $b){ return $a - $b; });
print_r($arr);
?>
Array ( [0] => -2 [1] => 1 [2] => 5 [3] => 10 )
Отличие: callback должен возвращать отрицательное/0/положительное значение, похож на Java, но нет строгой типизации.
JavaScript
// Node.js
let arr = [5,1,10,-2];
arr.sort((a,b) => a - b);
console.log(arr);
[ -2, 1, 5, 10 ]
Отличие: compare-функция возвращает число, используется встроенный Array.prototype.sort; по умолчанию сравнение как строк.
Python
# Python 3
arr = [5,1,10,-2]
# Рекомендуется key
arr.sort()
print(arr)
# Если нужен cmp-подход
from functools import cmp_to_key
arr = [5,1,10,-2]
arr.sort(key=cmp_to_key(lambda a,b: (a>b) - (a<b)))
print(arr)
[-2, 1, 5, 10] [-2, 1, 5, 10]
Отличие: в современном Python предпочтителен key-функция (экстракция ключа) вместо cmp.
SQL
-- SQL: ORDER BY
SELECT name FROM users ORDER BY age ASC;
-- Результат зависит от данных в таблице
Отличие: сравнение встроено в СУБД, нет явной функции-колбэка в стиле Java. Для сложных правил можно использовать пользовательские функции.
C#
using System;
using System.Collections.Generic;
class Example { static void Main(){ var arr = new List<int>{5,1,10,-2}; arr.Sort((a,b) => a.CompareTo(b)); Console.WriteLine(string.Join(",",arr)); } }
-2,1,5,10
Отличие: есть IComparer<T> и Comparison<T> делегат; поведение близко к Java.
Lua
arr = {5,1,10,-2}
table.sort(arr, function(a,b) return a < b end)
for i,v in ipairs(arr) do print(v) end
-2 1 5 10
Отличие: функция возвращает boolean (true если a < b), а не целочисленное значение.
Go
package main
import (
"fmt"
"sort"
)
func main(){ arr := []int{5,1,10,-2}
sort.Slice(arr, func(i,j int) bool { return arr[i] < arr[j] })
fmt.Println(arr)
}
[-2 1 5 10]
Отличие: в Go компаратор - булева функция less(i,j), возвращающая true, если элемент i должен быть раньше j.
Kotlin
data class P(val name:String, val age:Int)
fun main(){ val list = mutableListOf(P("A",2),P("B",1))
list.sortWith(compareBy<P>{it.age})
println(list)
}
[P(name=B, age=1), P(name=A, age=2)]
Отличие: Kotlin использует совместимые с Java Comparator утилиты, синтаксис более лаконичен.
Типичные ошибки и примеры
1. Вычитание чисел как возвращаемое значение приводит к переполнению:
import java.util.*;
class Err1 {
public static void main(String[] args) {
List<Integer> l = Arrays.asList(Integer.MAX_VALUE, -1);
l.sort((a,b) -> a - b); // потенциальное переполнение
System.out.println(l);
}
}
[ -1, 2147483647 ] // иногда некорректный порядок при переполнении
Рекомендация: использовать Integer.compare(a,b).
2. Непоследовательный компаратор (нарушение транзитивности) приводит к неустойчивому поведению сортировщиков:
import java.util.*;
class Err2 {
static Comparator<String> bad = (a,b) -> {
if(a.length() > b.length()) return 1;
if(a.length() < b.length()) return -1;
return a.equals(b) ? 0 : 1; // не транзитивно относительно третьих строк
};
public static void main(String[] args){
List<String> l = Arrays.asList("a","bb","c");
Collections.sort(l, bad);
System.out.println(l);
}
}
Поведение сортировки неопределено; возможны неожиданные результаты или исключения в коллекциях.
3. Comparator не согласован с equals при использовании в TreeSet:
import java.util.*;
class Err3 {
static class P{ int id; P(int i){id=i;} public String toString(){return "P"+id;} }
public static void main(String[] args){
Comparator<P> cmp = (a,b) -> 0; // все элементы считаются равными по порядку
TreeSet<P> ts = new TreeSet<>(cmp);
ts.add(new P(1));
ts.add(new P(2));
System.out.println(ts.size());
}
}
1 // потеря элементов, т.к. все считаются одинаковыми по порядку
4. NullPointerException при прямом вызове compareTo без проверки null:
import java.util.*;
class Err4 { public static void main(String[] args){ List<String> l = Arrays.asList("a", null);
l.sort((x,y) -> x.compareTo(y)); // NPE
}}
Exception in thread "main" java.lang.NullPointerException at Err4.lambda... (пример)
Изменения и эволюция Comparator
Крупные улучшения появились в Java 8: введены статические и default-методы, упрощающие создание и комбинирование компараторов. Среди добавленных утилит:
- Comparator.comparing, comparingInt, comparingLong, comparingDouble - для создания компараторов по ключам.
- thenComparing - для последовательного комбинирования критериев.
- nullsFirst, nullsLast - для явной обработки null.
- reversed - для обратного порядка.
- naturalOrder, reverseOrder - готовые компараторы для натурального порядка.
Сигнатура int compare(T,T) при этом осталась неизменной. В последующих версиях Java интерфейс не претерпел изменений, влияющих на сам метод compare; развитие сосредоточено на утилитах и улучшениях Stream API.
Расширенные и нестандартные примеры
1. Locale-aware сравнение строк через Collator:
import java.util.*;
import java.text.*;
class Adv1{
public static void main(String[] args){
List<String> l = Arrays.asList("ångström","zebra","ångb");
Collator c = Collator.getInstance(new Locale("sv")); // шведская локаль
l.sort(c);
System.out.println(l);
}
}
[ångb, ångström, zebra] // порядок зависит от локали
2. Сортировка по квадрату расстояния (для оптимизации без sqrt):
import java.util.*;
class Point{double x,y; Point(double x,double y){this.x=x;this.y=y;} public String toString(){return "("+x+","+y+")";} }
class Adv2{
public static void main(String[] args){
List<Point> pts = Arrays.asList(new Point(0,2), new Point(1,1), new Point(2,0));
pts.sort(Comparator.comparingDouble(p -> p.x*p.x + p.y*p.y));
System.out.println(pts);
}
}
[(1.0,1.0), (0.0,2.0), (2.0,0.0)]
3. Предвычисление ключей (Schwartzian transform) для дорогой операции извлечения ключа:
import java.util.*;
class Expensive{ String s; Expensive(String s){this.s=s;} int key(){ try{ Thread.sleep(50); } catch(Exception e){} return s.length()*s.hashCode(); } public String toString(){return s;} }
class Adv3{
public static void main(String[] args){
List<Expensive> list = Arrays.asList(new Expensive("aaa"), new Expensive("bb"));
// Предвычисление ключей
List<Map.Entry<Expensive,Integer>> temp = new ArrayList<>();
for(Expensive e : list) temp.add(new AbstractMap.SimpleEntry<>(e, e.key()));
temp.sort(Comparator.comparingInt(Map.Entry::getValue));
List<Expensive> sorted = new ArrayList<>();
for(var en : temp) sorted.add(en.getKey());
System.out.println(sorted);
}
}
[bb, aaa] // ключи вычислены один раз, эффективнее при дорогой key()
4. Comparator для корректной работы с NaN и -0.0/+0.0 у double:
import java.util.*;
class Adv4{
public static void main(String[] args){
List<Double> vals = Arrays.asList(Double.NaN, -0.0, 0.0, 1.0);
vals.sort(Double::compare); // Double.compare правильно обрабатывает NaN и знаки нуля
System.out.println(vals);
}
}
[0.0, -0.0, 1.0, NaN]
5. Использование компаратора в параллельном потоке и влияние на стабильность:
import java.util.*;
import java.util.stream.*;
class Adv5{
public static void main(String[] args){
List<Integer> list = Arrays.asList(5,1,3,2,4);
List<Integer> sorted = list.parallelStream().sorted(Comparator.naturalOrder()).collect(Collectors.toList());
System.out.println(sorted);
}
}
[1, 2, 3, 4, 5] // использует параллельный алгоритм, но результат отсортирован
6. Последствия неконсистентного компаратора в TreeSet (демонстрация):
import java.util.*;
class Adv6{
static class Item{int id; String v; Item(int i,String s){id=i;v=s;} public String toString(){return id+":"+v;} }
public static void main(String[] args){
Comparator<Item> cmp = Comparator.comparingInt((Item it) -> it.v.length());
TreeSet<Item> ts = new TreeSet<>(cmp);
ts.add(new Item(1, "a"));
ts.add(new Item(2, "b")); // length equal, считаются равными по порядку
System.out.println(ts); // потеря одного элемента
}
}
[1:a] // элемент с id 2 не добавлен из-за равенства порядка
Заключение: продуманные компараторы, использование утилит Comparator и внимательное отношение к null, NaN и переполнениям обеспечивают корректную и предсказуемую работу сортировок и структур данных.