Сравнение объектов с Comparator и Comparable

🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!

Несмотря на то, что кажется, будто эта тема обсуждалась уже не один раз, все же сегодняшнюю статью посвятим сравнению объектов.

Известно, как работает оператор ==, а также методы equals() и hashCode().

Но сравнение — это немного другое. Раньше, скорее всего, речь шла о “проверке объектов на равенство”.

Сравнение объектов

Однако причины для сравнения объектов друг с другом могут быть совершенно разными! Самая очевидная — сортировка.

Читайте также: Сравнение строк в Java

Вероятно, если бы вас попросили отсортировать ArrayList<> с числами или строками, вы бы без труда справились:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       String name1 = "Masha";
       String name2 = "Sasha";
       String name3 = "Dasha";

       List<String> names = new ArrayList<>();
       names.add(name1);
       names.add(name2);
       names.add(name3);

       Collections.sort(names);
       System.out.println(names);
   }
}
Run Code

Консольный вывод:

[Dasha, Masha, Sasha]

Если вы вспомнили класс Collections и его метод sort(), то вы молодцы! С числами у вас также вряд ли будут проблемы.

Вот задача посложнее:

public class Car {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   // ...getters, setters, toString()

}

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);
   }
}
Run Code

На самом деле задача простая: есть класс Car и три объекта этого класса.

Попробуйте их отсортировать в списке.

Возникает вполне логичный вопрос: “Как именно их следует сортировать? По названию, году выпуска или по максимальной скорости?”.

Отличный вопрос. На данный момент мы не знаем, как отсортировать объекты Car.

И, что логично, Java этого тоже не знает! Если попытаться передать список объектов Car в метод Collections.sort(), получим ошибку компиляции:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(20012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       // Compilation error!
       Collections.sort(cars);
   }
}
Run Code

И действительно, откуда языку знать, как сортировать объекты классов, которые вы написали сами?

Это зависит от того, для чего предназначена ваша программа.

Нужно каким-то образом научить Java сравнивать эти объекты. И сравнивать их именно так, как требуется нам.

В Java для этого есть специальный механизм — интерфейс Comparable.

Интерфейс Comparable в Java

Чтобы как-то сравнивать и сортировать объекты Car, класс должен реализовать интерфейс Comparable, который состоит из единственного метода: compareTo():

public class Car implements Comparable<Car> {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   @Override
   public int compareTo(Car o) {
       return 0;
   }

   // ...getters, setters, toString()

}
Run Code

Обратите внимание: мы указали Comparable<Car>, а не просто Comparable. Это параметризованный интерфейс, поэтому нужно указать конкретный тип — в нашем случае класс Car.

В принципе, можно убрать <Car>, но тогда сравнение по умолчанию будет основано на типе Object. В итоге метод будет выглядеть так:

@Override
   public int compareTo(Object o) {
       return 0;
   }
Run Code

Очевидно, что работать с Car гораздо удобнее.

Внутри метода compareTo() описана собственная логика сравнения автомобилей. Допустим, нужно отсортировать их по году выпуска.

Вы, наверное, заметили, что метод compareTo() возвращает int, а не boolean.

Не удивляйтесь. При сравнении двух объектов есть 3 варианта:

  • а < b
  • a > b
  • a == b.

У boolean есть только 2 значения: true и false, а этого недостаточно для сравнения объектов.

С int все гораздо проще. Если возвращаемое значение > 0, то a > b. Если результат < 0, то a < b. А если результат == 0, то два объекта равны: a == b.

Научить наш класс сортировать автомобили по году выпуска — проще простого:

@Override
public int compareTo(Car o) {
   return this.getManufactureYear() - o.getManufactureYear();
}
Run Code

Что же здесь происходит?

Мы берем текущий объект(this), получаем год выпуска этого автомобиля и вычитаем из него год выпуска другого автомобиля (o).

Если год выпуска первого автомобиля больше, метод вернет значение int > 0.

Это означает, что this > o.

И наоборот, если год выпуска второго автомобиля) больше, то метод вернет отрицательное число, что означает, что о > this.

Наконец, если они равны, то метод вернет 0.
Этого простого механизма уже достаточно для сортировки коллекций объектов Car!

Проверим:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       // There was previously an error here
       Collections.sort(cars);
       System.out.println(cars);
   }
}
Run Code

Консольный вывод:

[Car{manufactureYear=1990, model='Ferrari 360 Spider', maxSpeed=310},
Car{manufactureYear=2010, model='Bugatti Veyron', maxSpeed=350},
Car{manufactureYear=2012, model='Lamborghini Gallardo', maxSpeed=290}]

Машины отсортированы так, как мы и хотели! 🙂

Сортировка автомобилей

Когда следует использовать Comparable?

Метод сравнения, реализованный в Comparable, называют естественным порядком сортировки (natural ordering). Это связано с тем, что в методе compareTo() вы определяете наиболее распространенный, или естественный, способ сравнения объектов данного класса.

В Java уже есть примеры естественного порядка. Например, строки чаще всего сортируются по алфавиту, а числа — по возрастанию. Поэтому при вызове метода sort() для списка чисел или строк, они будут отсортированы.

Если наша программа обычно сравнивает и сортирует автомобили по году выпуска, логично определить для них естественную сортировку с помощью интерфейса Comparable<Car> и метода compareTo().

Но что, если этого недостаточно?

Представим, что программа не так проста.

В большинстве случаев подойдет естественная сортировка автомобилей (в нашем случае — по году выпуска).

Но иногда клиенты — любители быстрой езды. Если мы готовим каталог автомобилей для них, то машины должны быть отсортированы по максимальной скорости.

Когда следует использовать Comparable на примере сортировки автомобилей

Допустим, так нужно всего в 15% случаев. Этого явно недостаточно для того, чтобы менять естественный порядок сортировки Car с года выпуска на скорость.

Но игнорировать 15 % клиентов тоже нельзя. Что же делать в таком случае?

Здесь на помощь приходит другой интерфейс — Comparator. Как и Comparable, он является параметризованным интерфейсом.

Чем они отличаются?

Comparable делает объекты “сопоставимыми” и определяет их естественный порядок сортировки, который используется в большинстве случаев.

Comparator — это отдельный интерфейс-сравниватель.

Если нужно реализовать какой-то особый порядок сортировки, не нужно трогать класс Car и менять логику compareTo().

Вместо этого можно создать отдельный класс, реализующий Comparator, и научить его выполнять нужную сортировку!

import java.util.Comparator;

public class MaxSpeedCarComparator implements Comparator<Car> {

   @Override
   public int compare(Car o1, Car o2) {
       return o1.getMaxSpeed() - o2.getMaxSpeed();
   }
}
Run Code

Как видите, Comparator довольно прост.

Нужно реализовать только один метод интерфейса: compare(). Он принимает в качестве входных данных два объекта Car и сравнивает их максимальные скорости обычным способом (вычитанием). Как и compareTo(), он возвращает int, по тому же принципу.

Как это применить? Довольно просто:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Car> cars = new ArrayList<>();

       Car ferrari = new Car(1990, "Ferrari 360 Spider", 310);
       Car lambo = new Car(2012, "Lamborghini Gallardo", 290);
       Car bugatti = new Car(2010, "Bugatti Veyron", 350);

       cars.add(ferrari);
       cars.add(bugatti);
       cars.add(lambo);

       Comparator speedComparator = new MaxSpeedCarComparator();
       Collections.sort(cars, speedComparator);

       System.out.println(cars);
   }
}
Run Code

Консольный вывод:

[Car{manufactureYear=2012, model='Lamborghini Gallardo', maxSpeed=290},
Car{manufactureYear=1990, model='Ferrari 360 Spider', maxSpeed=310},
Car{manufactureYear=2010, model='Bugatti Veyron', maxSpeed=350}]

Мы просто создаем объект-компаратор и передаем его в метод Collections.sort() вместе со списком, который нужно отсортировать.

Когда метод sort() получает компаратор, он не использует естественную сортировку, заданную в методе compareTo() класса Car. Вместо этого применяется алгоритм сравнения, реализованный в переданном компараторе.
В чем преимущества такого подхода?

Во-первых, совместимость с существующим кодом. Создан новый, специальный метод сортировки, но при этом сохранился существующий, который будет использоваться по умолчанию.

Класс Car при этом вообще остается нетронутым. Он как реализовывал Comparable, так и продолжает это делать:

public class Car implements Comparable<Car> {

   private int manufactureYear;
   private String model;
   private int maxSpeed;

   public Car(int manufactureYear, String model, int maxSpeed) {
       this.manufactureYear = manufactureYear;
       this.model = model;
       this.maxSpeed = maxSpeed;
   }

   @Override
   public int compareTo(Car o) {
       return this.getManufactureYear() - o.getManufactureYear();
   }

   // ...getters, setters, toString()

}
Run Code

Во-вторых, гибкость. Можно добавить столько алгоритмов сортировки, сколько нужно.

Например, сортировать машины по цвету, скорости, весу или даже по тому, сколько раз конкретная модель появлялась в фильмах про Бэтмена. Все, что нужно — это создать дополнительный Comparator.

Продвинутая сортировка с помощью Comparator в Java

Скорее всего вы уже уверенно чувствуете себя в основах интерфейса Comparator в Java. Но подождите — знаете ли вы, что Comparator гораздо большее, чем просто сортировка чисел по возрастанию или строк по алфавиту? Да-да! Он может обрабатывать специальные правила сортировки, динамическую сортировку, работать со сложными объектами и даже использовать элегантные лямбда-выражения. Раскроем же весь потенциал Comparator!

1. Особые правила сортировки строк и чисел

Иногда сортировка по умолчанию просто не подходит. Бывают случаи, когда нужно отсортировать числа по убыванию или расположить строки по их длине. Звучит сложно? Вовсе нет!

Сортировка чисел в порядке убывания

import java.util.*;

public class DescendingSortExample {
    public static void main(String[] args) {
        List numbers = Arrays.asList(5, 2, 9, 1, 7);

        // Sorting numbers in descending order
        numbers.sort(Comparator.reverseOrder());

        System.out.println("Descending order: " + numbers);  // Output: [9, 7, 5, 2, 1]
    }
}
Run Code

Сортировка строк по длине

import java.util.*;

public class StringLengthSortExample {
    public static void main(String[] args) {
        List words = Arrays.asList("apple", "kiwi", "banana", "pear");

        // Sorting strings by their length
        words.sort(Comparator.comparingInt(String::length));

        System.out.println("Sorted by length: " + words);  // Output: [kiwi, pear, apple, banana]
    }
}
Run Code

Довольно круто, правда? Всего несколько строк кода — и сортировка работает именно так, как вам нужно.

2. Динамическая сортировка с помощью Comparator

Представьте, что вы создаете приложение, в котором пользователи могут выбирать, как им сортировать данные — по имени, по дате, по размеру. Как это реализовать? Очень просто! Comparator позволяет менять правила сортировки буквально на лету.

Пример: переключение правил сортировки

import java.util.*;

public class DynamicSortingExample {
    public static void main(String[] args) {
        List items = Arrays.asList("apple", "kiwi", "banana", "pear");

        Scanner scanner = new Scanner(System.in);
        System.out.println("Sort by (1) Alphabet or (2) Length:");
        int choice = scanner.nextInt();

        if (choice == 1) {
            items.sort(String::compareTo);  // Alphabetical order
        } else if (choice == 2) {
            items.sort(Comparator.comparingInt(String::length));  // By length
        }

        System.out.println("Sorted list: " + items);
    }
}
Run Code

Теперь ваша программа может подстраиваться под предпочтения пользователя. Это уже совсем другой уровень гибкости.

3. Пользовательские правила сортировки для сложных объектов

Сортировка примитивных типов — это одно. Но что делать с объектами? Допустим, у вас есть список книг, и нужно отсортировать их по названию, автору или цене. Да нет проблем!

Пример: сортировка списка книг

import java.util.*;

class Book {
    String title;
    String author;
    double price;

    Book(String title, String author, double price) {
        this.title = title;
        this.author = author;
        this.price = price;
    }

    @Override
    public String toString() {
        return title + " by " + author + " - $" + price;
    }
}

public class BookSortingExample {
    public static void main(String[] args) {
        List books = Arrays.asList(
            new Book("Java Basics", "Alice", 29.99),
            new Book("Advanced Java", "Bob", 49.99),
            new Book("Spring Boot", "Charlie", 39.99)
        );

        // Sort by price
        books.sort(Comparator.comparingDouble(book -> book.price));

        System.out.println("Sorted by price:");
        books.forEach(System.out::println);
    }
}
Run Code

Вывод в консоли:

Java Basics by Alice - $29.99
Spring Boot by Charlie - $39.99
Advanced Java by Bob - $49.99

Как видите, можно сортировать по любому полю объекта. Вы сами определяете, как должны вести себя данные.

4. Упрощение с помощью лямбда-выражений

А теперь сделаем все еще проще. Почему бы не отказаться от шаблонного кода, если можно использовать лямбда-выражения? Это чище, короче и легче читается.

Пример: сортировка с помощью лямбд

import java.util.*;

public class LambdaSortingExample {
    public static void main(String[] args) {
        List items = Arrays.asList("apple", "kiwi", "banana", "pear");

        // Sort alphabetically using lambda
        items.sort((a, b) -> a.compareTo(b));
        System.out.println("Alphabetical: " + items);

        // Sort by length using lambda
        items.sort((a, b) -> Integer.compare(a.length(), b.length()));
        System.out.println("By length: " + items);
    }
}
Run Code

Коротко, ясно и все по делу. Именно поэтому лямбды так сильно изменили подход к коду в Java.

Вот и все! Мы разобрали два очень важных механизма, которые будут регулярно встречаться вам в реальных проектах.

Перевод статьи «Java Comparator and Comparable».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх