Выбор между Comparator и Comparable

    Для реализации сортировки требуется, чтобы сортируемые объекты можно было сравнивать между собой на больше-меньше. Иначе говоря, чтобы было определено правило, которое позволит для любых двух объектов указать, какой из них в рамках данного контекста идет раньше, а какой позже.

    В java эти правила определяются на уровне классов, к которым принадлежат объекты. Для примера возьмем класс для описания счета пользователя:

    UserAccount {
      currency
      value
      updatedTimestamp
    }

    В зависимости от контекста сравнение счетов пользователя может происходить по разным правилам, например:

    • в приложении пользователь видит счета, отсортированные по currency, потом по value;

    • в админке счета всех пользователей отсортированы по дате изменения.

    Для реализации сравнения на больше-меньше в java предусмотрено две возможности:

    • UserAccount реализует интерфейс Comparable<UserAccount> В этом случае два объекта получают возможность сравнения между собой: acc1.compareTo(acc2)

    • Создается отдельный класс, реализующий интерфейс Comparator<UserAccount>, и дальше объект этого класса может сравнить между собой два объекта исходного класса: userAccountComparator.compare(acc1, acc2)

    Очевидно, что в некоторых случаях выбора между Comparable и Comparator нет. Если исходный класс нельзя модифицировать, или если требуются разные правила сравнения, то придется использовать Comparator. В остальных случаях, технически, можно использовать как Comparator, так и Comparable.

    Согласно документации использование Comparable возможно, если для сравнения используется естественный порядок (class's natural ordering). По ссылке есть представление разработчиков, что такое естественный порядок для стандартных классов (String, Date). Мне не удалось выяснить, что такое естественный порядок в общем случае. Выглядит, что это интуитивное представление разработчика о порядке в данном контексте. При этом даже для стандартных классов порядок может быть не интуитивен (в каком порядке должны идти значения BigDecimal с разной точностью, например 4.0 и 4.00?). Практика показывает, что "естественность" правил различается в понимании разных разработчиков и контекстов. Для меня необходимость опоры на интуицию является аргументом против использования Comparable.

    При написании кода приходится лавировать между явностью и неявностью. Явность хороша тем, что для понимания участка кода требуется рассмотреть лишь узкий контекст его использования. В то же время неявность позволяет использовать одинаковые правила для всех участков кода. Разберем на примере: разработчику требуется отсортировать список счетов. Правила сравнения можно указать:

    • неявно: Arrays.sort(accountsList)

    • явно: Arrays.sort(accountsList, accountByValueComparator)

    Чтобы разработчику узнать правила сортировки в первом случае ему нужно будет посмотреть, как реализован метод compareTo у класса UserAccount, а во втором - у accountByValueComparator. Здесь разницы нет. Но посмотрим на обратную задачу. Пусть разработчик видит перед собой код

    UserAccount implements Comparable<UserAccount> {
      @Override
      public int compareTo(UserAccount other) { .. }
    }

    Как определить, где используется объявленный метод compareTo ? Если искать по использованию метода, то потребуется включить в поиск и метод суперкласса Comparable#compareTo, так как именно он будет найден в Arrays.sort(). Но метод суперкласса используется также в огромном количестве других мест внутри кода jdk, и найти таким образом его будет сложно. Я не нашел способов искать такие использования методов кроме как ручным перебором: ищем все включения класса UserAccount и ниже по стеку обращаем внимание на все Arrays.sort(), stream.sorted() и тд.

    В случае явной передачи компаратора найти его использования элементарно. Я считаю это аргументом за использование компаратора. (Еще одним примером сложностей с поиском неявных использований может служить equals/hashCode, но альтернативы в виде "Equalator"-а для них нет).

    Резюмируя - в большинстве случаев аргументы за использование Comparator перевешивают аргументы за использование Comparable.

    Похожие публикации

    Средняя зарплата в IT

    111 111 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 6 720 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 14

      +1
      Согласно документации использование Comparable возможно, если для сравнения используется естественный порядок (class's natural ordering).


      Документация (SE 8):
      public interface ComparableThis interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering, and the class's compareTo method is referred to as its natural comparison method.


      А можете пальцем показать, где вы тут нашли «возможно, если»? Вот тут не так написано. Тот порядок, который задается Comparable, называется естественным. Если вам такой порядок нужен — используйте a.compareTo(B). Если нет — пишете компаратор.
        0
        Согласен, недостаточно внимательно прочитал. Получается, документация дает определение: «естественный порядок» это порядок, заданный в Comparable.
        Однако это не дает формальных правил чтобы разработчик смог понять, может он он в своем классе имплементировать Comparable или нет.
          0
          Ну я бы так сказал — если у вас есть естественный порядок (он всегда один) — то вам подойдет Comparable. Ну то есть, понятно как сортировать даты, строки, числа — у них у всех есть его реализация. Но если вы хотите сортировать строки без учета больших и маленьких букв — то компаратор наше все.
        0

        для меня использование .compareTo омрачается необходимостью проверки на NULL каждый раз.
        с java знаком шапочно, может быть я что-то не так понимаю.

          0
          Обычно в пользовательском коде не требуется явно вызвать .compareTo, это делают за нас сортировщики у коллекций, стримов и прочего. И уже для них специфицируется возможность работы с nullable значениями, а она нужна не только для сравнения. Так что необходимость проверки на null при сортировке обычно не является проблемой.
          Также в этой статье я не затрагивал проблему total order, когда compareTo возвращает 0. Так как она одинаковая для обоих интерфейсов сравнения.
            0
            Обычно в пользовательском коде не требуется явно вызвать .compareTo, это делают за нас сортировщики у коллекций, стримов и прочего.

            хорошо, а как тогда сравнивать «в пользовательском коде»?


            private void someMethod(BigDecimal b) {
                if ( b.compareTo(BigDecimal.ZERO) <= 0 ) {
                    // value must be positive, abort
                }
                // do something
            }

            приходится или вставлять проверку на NULL (что загрязняет код), или «переворачивать» условие (что опять же плохо читается), или писать отдельную функцию-компаратор.


            P. S. впрочем, понятно, что тут дело не в compareTo, а в обработке NULL в java. может быть оно сделано правильно, но мне «не зашло» )

              0
              Если хотите вызывать любой метод объекта по ссылке first, в т.ч. first.compareTo(second), и эта ссылка может содержать значение null, то нужно обрабатывать случаи first = null и first != null как это делаете Вы.
              Более того, если second может содержать null, тогда или перед вызовом метода compareTo(), или внутри него, нужно предусмотреть обработку случаев с second = null и second != null.
              Можно также написать компаратор, который обрабатывает оба значения как nullable. В целом это не проблема сравнения, это проблема обработки nullable ссылок.
              Было бы интересно посмотреть контекст, в котором Вы используете этот метод. Если его приходится дергать руками, то есть ли смысл вообще делать свой класс реализацией Comparable?
                0

                речь в данном случае не про свой класс, а про BigDecimal (ну и не только, но сравнений BigDecimal в коде много и выглядит на мой взгляд это «так себе»).


                да, мой комментарий не имеет отношения к статье )


                В целом это не проблема сравнения, это проблема обработки nullable ссылок.

                это понятно. непонятно как писать красиво.

                  0
                  Я ошибочно обобщил Ваш вопрос и даже пример кода. Поискал по текущему рабочему проекту использования compareTo, и понял что оно действительно активно используется при работе с BigDecimal.
                  В целом подход проверки на null остается тот же. Если везде логика обработки разная, то придется в каждом месте проверять условие. Если логику можно сделать одинаковой в разных местах, то следует вынести ее в отдельный метод. Реализовать этот метод в виде утилитного в текущем классе с бизнес логикой, или же в виде компаратора — дело вкуса.
                +1
                или «переворачивать» условие (что опять же плохо читается), или писать отдельную функцию-компаратор

                Если посмотреть в код компаратора BigDecimal, можно увидеть, что переворот условия не сработает.
            0

            А что скажите про thenComparing у компараторов?
            Я это к чему? При казалось бы похожей сфере применимости, у Comparator и Comparable, все-таки есть случаи, когда их возможности расходятся. Именно поэтому существуют различные языковые конструкции.

              0
              Мне кажется этот вопрос больше относится к наличию утилитных методов для удобной реализации сравнения. С 1.8 такие методы есть для chaining-а компараторов, с 1.7 есть методы вроде статического Integer#compare(x,y)
              0
              Вы пишите, что для реализации Comparator надо создавать отдельный класс, а для Comparable якобы не надо.
              И то и другое являются интерфейсами разных пакетов. И одно и второе может имплементить в один и тот же класс.
              Данная статья хорошо подходит джунам, но для джунов надо сначала разжевать, что такое Comparator, потом Comparable, привести пример с созданием классов и имплементации. И выкинуть
              неявно: Arrays.sort(accountsList)
              явно: Arrays.sort(accountsList, accountByValueComparator)

              и статья станет не плохой. Доработайте, пожалуйста)

                0
                Могли бы Вы уточнить?
                Технически можно сделать чтобы класс имплементировал для самого себя интерфейс компаратора (или валидатора, сериализатора). На практике это явное слияние зон ответственности, и вряд ли будет использоваться. Вопрос в том чтобы убрать слово «отдельный»?
                Как без использования примеров явно/неявно подвести к проблеме поиска использований определенного метода compareTo?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое