Сортировка в Java делается встроенными методами: list.sort(comparator) или Collections.sort(list) для списков, Arrays.sort(array) для массивов, stream().sorted() для потоков данных. Как сравнивать элементы, Java узнает из интерфейса Comparable (естественный порядок, встроенный в класс) или Comparator (своя логика для конкретного случая). Раньше разработчики писали свои алгоритмы сортировки (пузырьковая, быстрая и так далее), но сейчас это делать не нужно: в Java уже есть готовые, оптимизированные решения со сложностью O(n log n). Давай разберем, как ими пользоваться.

Кратко

  • Списки сортируют методом list.sort(comparator) (с Java 8) или Collections.sort(list): оба меняют список на месте, причем второй с Java 8 просто вызывает первый.
  • Массивы, включая примитивные, сортирует Arrays.sort(array); исходный массив при этом меняется, для копии сначала Arrays.copyOf().
  • Stream API (stream().sorted()) не трогает исходную коллекцию и возвращает новый список.
  • Порядок сравнения задает Comparable (метод compareTo() внутри класса, один естественный порядок) или Comparator (отдельный объект, любой порядок; с лямбдами и Comparator.comparing() это одна строка, многоуровневая сортировка через thenComparing()).
  • TreeSet и TreeMap держат элементы отсортированными всегда: поиск, вставка и удаление за O(log n).
  • Свои алгоритмы сортировки в рабочем коде не нужны: встроенные методы работают за O(n log n) и стабильны, а ручной двойной цикл дает O(n²) и лишние баги.
Разноцветные деревянные чашки, выстроенные по размеру от большой к маленькой, и россыпь цветных камешков рядом: наглядная аналогия сортировки

Основы: Comparable и Comparator

Прежде чем начать сортировать, нужно понять главное: Java должна знать, как сравнивать объекты. Для чисел это очевидно (5 меньше 10), а для твоих собственных классов нет. Тут на помощь приходят два интерфейса:
  • Comparable: для естественного порядка сортировки (встраивается в класс)
  • Comparator: для кастомной логики сортировки (отдельный объект)

Интерфейс Comparable

Если твой класс имеет "естественный" порядок сортировки (например, студенты по имени), реализуй интерфейс Comparable:
class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(Student other) {
        return this.name.compareTo(other.name); // по имени
    }
}
Метод compareTo() должен возвращать:
  • Отрицательное число, если текущий объект меньше
  • Ноль, если объекты равны
  • Положительное число, если текущий объект больше

Интерфейс Comparator

Когда нужна гибкость (например, сортировать то по имени, то по возрасту), используй Comparator. С Java 8 это стало невероятно удобно благодаря лямбдам:
List<Student> students = Arrays.asList(
    new Student("Анна", 22),
    new Student("Борис", 20),
    new Student("Виктор", 21)
);

// Сортировка по возрасту (лямбда)
students.sort((s1, s2) -> Integer.compare(s1.getAge(), s2.getAge()));

// Еще проще: через method reference
students.sort(Comparator.comparingInt(Student::getAge));
// Результат: Борис(20), Виктор(21), Анна(22)

Сортировка коллекций

Метод Collections.sort()

Классический способ: использовать статический метод Collections.sort(). Он работает со списками (List):
List<Integer> numbers = new ArrayList<>(Arrays.asList(5, 2, 8, 1, 9));
Collections.sort(numbers);
System.out.println(numbers);
// Вывод: [1, 2, 5, 8, 9] Для обратного порядка используй Collections.reverseOrder():
Collections.sort(numbers, Collections.reverseOrder());
System.out.println(numbers);
// Вывод: [9, 8, 5, 2, 1]

Метод List.sort(): современный подход

С Java 8 у списков появился собственный метод sort(), который удобнее:
List<String> fruits = Arrays.asList("Яблоко", "Банан", "Апельсин");

// Естественный порядок
fruits.sort(null); // или Comparator.naturalOrder()
System.out.println(fruits);
// Вывод: [Апельсин, Банан, Яблоко] По длине строки:
fruits.sort(Comparator.comparingInt(String::length));
System.out.println(fruits);
// Вывод: [Банан, Яблоко, Апельсин]

Сортировка массивов

Для массивов используй Arrays.sort(), он работает с любыми типами данных:
// Примитивы
int[] numbers = {5, 2, 8, 1, 9};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers));
// Вывод: [1, 2, 5, 8, 9] С объектами и Comparator:
String[] words = {"Зебра", "Анаконда", "Бабочка"};
Arrays.sort(words, (a, b) -> Integer.compare(a.length(), b.length()));
System.out.println(Arrays.toString(words));
// Вывод: [Зебра, Бабочка, Анаконда] Важный момент: Arrays.sort() изменяет исходный массив. Если нужно сохранить оригинал, сделай копию:
int[] original = {5, 2, 8};
int[] sorted = Arrays.copyOf(original, original.length);
Arrays.sort(sorted);

Stream API: функциональный подход

Stream API из Java 8 позволяет сортировать данные в функциональном стиле, не изменяя исходную коллекцию:
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9);

List<Integer> sorted = numbers.stream()
    .sorted()
    .collect(Collectors.toList());

System.out.println("Исходный: " + numbers);
System.out.println("Отсортированный: " + sorted);
// Исходный: [5, 2, 8, 1, 9] // Отсортированный: [1, 2, 5, 8, 9] С Java 16 есть вариант короче: .toList() вместо .collect(Collectors.toList()). Разница в том, что toList() возвращает неизменяемый список: добавить в него элемент уже не получится, вызов закончится UnsupportedOperationException. С кастомной логикой:
List<String> names = Arrays.asList("Алексей", "Мария", "Иван");

List<String> sortedByLength = names.stream()
    .sorted(Comparator.comparingInt(String::length).reversed())
    .collect(Collectors.toList());

System.out.println(sortedByLength);
// Вывод: [Алексей, Мария, Иван] (от длинных к коротким)

Самосортирующиеся структуры данных

Иногда нужно, чтобы данные всегда были отсортированы. Для этого используй TreeSet и TreeMap: они автоматически поддерживают порядок элементов.

TreeSet: отсортированное множество

Set<Integer> numbers = new TreeSet<>();
numbers.add(5);
numbers.add(2);
numbers.add(8);
numbers.add(1);

System.out.println(numbers);
// Вывод: [1, 2, 5, 8] (автоматически отсортировано) С кастомным компаратором (обратный порядок):
Set<String> words = new TreeSet<>(Collections.reverseOrder());
words.add("Яблоко");
words.add("Банан");
words.add("Апельсин");

System.out.println(words);
// Вывод: [Яблоко, Банан, Апельсин] (в обратном алфавитном порядке)

TreeMap: отсортированный словарь

TreeMap сортирует пары ключ-значение по ключу:
Map<String, Integer> scores = new TreeMap<>();
scores.put("Алексей", 95);
scores.put("Мария", 87);
scores.put("Иван", 92);

scores.forEach((name, score) ->
    System.out.println(name + ": " + score)
);
// Алексей: 95 // Иван: 92 // Мария: 87 Важно: операции поиска, вставки и удаления в TreeSet/TreeMap имеют сложность O(log n), это быстрее линейного поиска по ArrayList или LinkedList. И еще одна деталь: равенство элементов эти структуры проверяют только через compareTo() или компаратор, а не через equals(). Если сравнение вернуло 0, второй элемент просто не добавится: TreeSet с компаратором по длине строки из «Банан» и «Слива» оставит только одно слово.

Практические примеры

Сортировка по нескольким полям

class Employee {
    private String department;
    private String name;
    private int salary;
    // конструктор, геттеры
}

List<Employee> employees = // ... заполнение списка

// Сортировка: отдел → зарплата (убывание) → имя
employees.sort(
    Comparator.comparing(Employee::getDepartment)
        .thenComparing(Employee::getSalary, Comparator.reverseOrder())
        .thenComparing(Employee::getName)
);

Сортировка Map по значениям

По умолчанию TreeMap сортирует только по ключам. Чтобы отсортировать по значениям, проще всего пропустить пары через Stream API и сложить результат в LinkedHashMap, который помнит порядок вставки:
Map<String, Integer> unsortedMap = new HashMap<>();
unsortedMap.put("Анна", 87);
unsortedMap.put("Борис", 95);
unsortedMap.put("Виктор", 82);

Map<String, Integer> sortedByValue = unsortedMap.entrySet().stream()
    .sorted(Map.Entry.comparingByValue())
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (e1, e2) -> e1,
        LinkedHashMap::new
    ));
// {Виктор=82, Анна=87, Борис=95}

Что НЕ нужно делать

Главное правило: не пиши свои алгоритмы сортировки, если в этом нет крайней необходимости. Вот пример того, что встречается в старом коде, но делать этого не стоит:
// ❌ Плохой подход: ручная сортировка обменом за O(n²)
double[] arr = {5.2, 2.1, 8.4, 1.7};
for (int i = 0; i < arr.length; i++) {
    for (int j = i + 1; j < arr.length; j++) {
        if (arr[j] < arr[i]) {
            double temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
}
Почему это плохо:
  • Низкая производительность: сложность O(n²) против O(n log n) у встроенных методов
  • Больше кода: 10 строк против одной
  • Больше ошибок: легко допустить баг в индексах
  • Плохая читаемость: другим разработчикам нужно время, чтобы понять логику
Правильный вариант занимает одну строку:
// ✅ Правильный подход
Arrays.sort(arr);
Исключение: писать свою сортировку имеет смысл только если ты делаешь что-то очень специфичное (например, сортируешь данные во время их чтения с диска для экономии памяти) или это учебная задача на собеседовании.

Подводим итоги

Вот краткая шпаргалка по сортировке в Java:
  • Списки: list.sort(comparator) или Collections.sort(list)
  • Массивы: Arrays.sort(array)
  • Stream API: stream().sorted().collect()
  • Всегда отсортировано: TreeSet, TreeMap
  • Кастомная логика: Comparator с лямбдами или method references
  • Многоуровневая: comparing().thenComparing()

Чем способы отличаются друг от друга

Все они дают одинаково отсортированный результат, разница в том, что именно сортируется и что происходит с исходными данными:
Способ Что сортирует Исходные данные Когда выбирать
list.sort(), Collections.sort() любой изменяемый List сортируются на месте обычная сортировка списка
Arrays.sort() массивы примитивов и объектов сортируются на месте, копия через Arrays.copyOf() массивы, в том числе int[]
stream().sorted() любой источник данных для Stream не меняются, результат в новом списке нужна копия или цепочка filter, map, sorted
TreeSet, TreeMap множество, словарь по ключу хранятся отсортированными постоянно данные все время меняются, а порядок нужен всегда
Java предоставляет мощные инструменты для сортировки. Используй их, и твой код будет чистым, быстрым и понятным. Удачи в программировании!

Вопросы и ответы

Чем Comparable отличается от Comparator?

Comparable реализуется внутри самого класса: метод compareTo() задает один естественный порядок, и его без дополнительных настроек используют Collections.sort(list), Arrays.sort(array), TreeSet и TreeMap. Comparator это отдельный объект, который описывает порядок снаружи: таких порядков может быть сколько угодно, и менять класс для этого не нужно. Если класс чужой (например, из библиотеки) и его код не поправить, остается только Comparator.

Что лучше: Collections.sort(list) или list.sort()?

Результат одинаковый. С Java 8 Collections.sort(list) по документации Oracle просто вызывает list.sort(null), то есть сортирует по естественному порядку тем же алгоритмом. list.sort(comparator) короче и сразу принимает компаратор, поэтому в новом коде обычно пишут его. Оба метода сортируют список на месте и гарантируют стабильность.

Стабильна ли сортировка в Java?

Для объектов да: Collections.sort(), List.sort(), Arrays.sort() для массивов объектов и sorted() у упорядоченного стрима гарантированно стабильны, то есть элементы, которые компаратор считает равными, сохраняют исходный взаимный порядок. Внутри там адаптивная сортировка слиянием (TimSort). Для массивов примитивов Arrays.sort(int[]) использует Dual-Pivot Quicksort, и вопрос стабильности там не стоит: два одинаковых числа неотличимы друг от друга.

Как отсортировать по убыванию?

Для естественного порядка передай Comparator.reverseOrder() (или его близнеца Collections.reverseOrder()): list.sort(Comparator.reverseOrder()), Arrays.sort(words, Comparator.reverseOrder()), stream().sorted(Comparator.reverseOrder()). У своего компаратора есть метод reversed(): Comparator.comparingInt(String::length).reversed(). Для массива примитивов вроде int[] обратного компаратора нет: либо отсортируй по возрастанию и обходи с конца, либо используй массив Integer[].

Почему строки сортируются не по алфавиту: слова с заглавной буквы идут раньше строчных?

Потому что compareTo() у String сравнивает не буквы, а их числовые коды в Unicode, а у всех заглавных букв коды меньше, чем у строчных: "яблоко" окажется после "Банан". Чтобы не зависеть от регистра, передай готовый компаратор String.CASE_INSENSITIVE_ORDER. Чтобы сортировать по правилам языка, как в словаре, используй java.text.Collator: list.sort(Collator.getInstance(Locale.forLanguageTag("ru"))).

Что будет, если в списке есть null?

NullPointerException: ни естественный порядок, ни обычный компаратор с null работать не умеют. Если null в данных допустим, оберни компаратор: list.sort(Comparator.nullsFirst(Comparator.naturalOrder())) поставит такие элементы в начало, Comparator.nullsLast(...) в конец. Для TreeSet и TreeMap это тоже работает, если передать такой компаратор в конструктор.

Читайте также