Comparator.compare: примеры (JAVA)

Примеры применения compare в Comparator
Раздел: Функциональные интерфейсы (использование и создание)
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:

Пример java
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):

Пример java
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) для дорогой операции извлечения ключа:

Пример java
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:

Пример java
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. Использование компаратора в параллельном потоке и влияние на стабильность:

Пример java
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 (демонстрация):

Пример java
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 и переполнениям обеспечивают корректную и предсказуемую работу сортировок и структур данных.

джава Comparator.compare function comments

En
Comparator.compare Compares its two arguments for order