Автор оригинала: François Dupire.
1. введение
Сравнение объектов является важной особенностью объектно-ориентированных языков программирования.
В этом уроке мы рассмотрим некоторые функции языка Java, которые позволяют нам сравнивать объекты. Кроме того, мы рассмотрим такие функции во внешних библиотеках.
2.
Давайте начнем с операторов == и != , которые могут определить, являются ли два объекта Java одинаковыми или нет, соответственно.
2.1. Примитивы
Для примитивных типов быть одинаковым означает иметь равные значения:
assertThat(1 == 1).isTrue();
Благодаря автоматической распаковке это также работает при сравнении примитивного значения с его аналогом типа оболочки :
Integer a = new Integer(1); assertThat(1 == a).isTrue();
Если два целых числа имеют разные значения, оператор == вернет false , а оператор != вернет true .
2.2. Объекты
Допустим, мы хотим сравнить два типа Integer wrapper с одним и тем же значением:
Integer a = new Integer(1); Integer b = new Integer(1); assertThat(a == b).isFalse();
При сравнении двух объектов значение этих объектов не равно 1. Скорее, это их адреса памяти в стеке , которые отличаются, поскольку оба объекта были созданы с использованием оператора new . Если бы мы назначили a |//b , то у нас был бы другой результат:
Integer a = new Integer(1); Integer b = a; assertThat(a == b).isTrue();
Теперь давайте посмотрим, что происходит, когда мы используем Integer#значение метода factory:
Integer a = Integer.valueOf(1); Integer b = Integer.valueOf(1); assertThat(a == b).isTrue();
В этом случае они считаются одинаковыми. Это связано с тем, что метод valueOf() хранит Целое число в кэше, чтобы избежать создания слишком большого количества объектов-оболочек с одним и тем же значением. Поэтому метод возвращает один и тот же экземпляр Integer для обоих вызовов.
Java также делает это для String :
assertThat("Hello!" == "Hello!").isTrue();
Однако, если бы они были созданы с помощью оператора new , то они не были бы одинаковыми.
Наконец, две null ссылки считаются одинаковыми, в то время как любой объект, не являющийся null , будет считаться отличным от null :
assertThat(null == null).isTrue(); assertThat("Hello!" == null).isFalse();
Конечно, поведение операторов равенства может быть ограничивающим. Что делать, если мы хотим сравнить два объекта, сопоставленные с разными адресами, и все же считать их равными на основе их внутренних состояний? Мы увидим это в следующих разделах.
3. Метод Object#equals
Теперь давайте поговорим о более широкой концепции равенства с помощью метода equals () .
Этот метод определен в классе Object , так что каждый объект Java наследует его. По умолчанию его реализация сравнивает адреса памяти объектов, поэтому он работает так же, как оператор == //. Однако мы можем переопределить этот метод, чтобы определить, что означает равенство для наших объектов.
Во-первых, давайте посмотрим, как он ведет себя для существующих объектов, таких как Integer :
Integer a = new Integer(1); Integer b = new Integer(1); assertThat(a.equals(b)).isTrue();
Метод по-прежнему возвращает true , когда оба объекта одинаковы.
Следует отметить, что мы можем передать объект null в качестве аргумента метода, но, конечно, не в качестве объекта, на котором мы вызываем метод.
Мы можем использовать метод equals() с нашим собственным объектом. Допустим, у нас есть Человек класс:
public class Person { private String firstName; private String lastName; public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } }
Мы можем переопределить метод equals() для этого класса, чтобы мы могли сравнить два Person s на основе их внутренних данных:
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person that = (Person) o; return firstName.equals(that.firstName) && lastName.equals(that.lastName); }
Для получения дополнительной информации см. статья на эту тему .
4. Объекты#равны Статическому методу
Теперь давайте рассмотрим Objects#equals статический метод . Ранее мы упоминали, что мы не можем использовать null в качестве значения первого объекта, иначе возникнет исключение NullPointerException .
То равняется() метод Объекты вспомогательный класс решает эти проблемы. Он принимает два аргумента и сравнивает их, также обрабатывая нулевой ценности.
Давайте снова сравним Человека объекты с:
Person joe = new Person("Joe", "Portman"); Person joeAgain = new Person("Joe", "Portman"); Person natalie = new Person("Natalie", "Portman"); assertThat(Objects.equals(joe, joeAgain)).isTrue(); assertThat(Objects.equals(joe, natalie)).isFalse();
Как мы уже говорили, метод обрабатывает значения null . Поэтому, если оба аргумента null , он вернет true , а если только один из них null , он вернет false .
Это может быть очень удобно. Допустим, мы хотим добавить необязательную дату рождения в наш Человек класс:
public Person(String firstName, String lastName, LocalDate birthDate) { this(firstName, lastName); this.birthDate = birthDate; }
Затем нам придется обновить наш метод equals () , но с обработкой null|/. Мы могли бы сделать это, добавив это условие в наш метод equals() :
birthDate == null ? that.birthDate == null : birthDate.equals(that.birthDate);
Однако, если мы добавим в наш класс много полей, допускающих обнуление, это может стать действительно грязным. Использование метода объекта#equals в нашей реализации equals() намного чище и улучшает читаемость:
Objects.equals(birthDate, that.birthDate);
5. Сопоставимый интерфейс
Логика сравнения также может использоваться для размещения объектов в определенном порядке. | Сопоставимый интерфейс позволяет нам определять порядок между объектами , определяя, является ли объект больше, равен или меньше другого.
Интерфейс Comparable является универсальным и имеет только один метод, compareTo() , который принимает аргумент универсального типа и возвращает int . Возвращаемое значение отрицательно, если this меньше аргумента, 0, если они равны, и положительно в противном случае.
Допустим, в нашем классе Person мы хотим сравнить объекты Person по их фамилии:
public class Person implements Comparable{ //... @Override public int compareTo(Person o) { return this.lastName.compareTo(o.lastName); } }
Метод compareTo() вернет отрицательный int , если вызывается с Человеком , имеющим большую фамилию, чем this , ноль, если та же фамилия, и положительный в противном случае.
Для получения дополнительной информации ознакомьтесь с нашим статья на эту тему .
6. Интерфейс компаратора
Comparator interface является универсальным и имеет метод compare , который принимает два аргумента этого универсального типа и возвращает целое число . Мы уже видели этот шаблон ранее с интерфейсом Comparable .
Компаратор аналогичен; однако он отделен от определения класса. Поэтому мы можем определить столько Компараторов , сколько нам нужно для класса, где мы можем предоставить только одну Сопоставимую реализацию.
Давайте представим, что у нас есть веб-страница, отображающая людей в виде таблицы, и мы хотим предложить пользователю возможность сортировать их по именам, а не по фамилиям. Это невозможно с Comparable , если мы также хотим сохранить нашу текущую реализацию, но мы могли бы реализовать наши собственные Компараторы .
Давайте создадим Person Компаратор , который будет сравнивать их только по именам:
ComparatorcompareByFirstNames = Comparator.comparing(Person::getFirstName);
Давайте теперь отсортируем Список людей, использующих этот Компаратор :
Person joe = new Person("Joe", "Portman"); Person allan = new Person("Allan", "Dale"); Listpeople = new ArrayList<>(); people.add(joe); people.add(allan); people.sort(compareByFirstNames); assertThat(people).containsExactly(allan, joe);
В интерфейсе Comparator есть другие методы, которые мы можем использовать в нашей реализации compareTo() :
@Override public int compareTo(Person o) { return Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName) .thenComparing(Person::getBirthDate, Comparator.nullsLast(Comparator.naturalOrder())) .compare(this, o); }
В этом случае мы сначала сравниваем фамилии, а затем имена. Затем мы сравниваем даты рождения, но поскольку они обнуляются, мы должны сказать как с этим справиться поэтому мы даем второй аргумент, говорящий, что их следует сравнивать в соответствии с их естественным порядком, но с нулевыми значениями, идущими последними.
7. Apache Commons
Теперь давайте взглянем на библиотеку Apache Commons . Прежде всего, давайте импортируем зависимость Maven :
org.apache.commons commons-lang3 3.11
7.1. Метод ObjectUtils#NotEqual
Во-первых, давайте поговорим о методе ObjectUtils#NotEqual . Требуется два аргумента Object , чтобы определить, не равны ли они, в соответствии с их собственной реализацией метода equals () . Он также обрабатывает значения null .
Давайте повторно используем наши примеры String :
String a = new String("Hello!"); String b = new String("Hello World!"); assertThat(ObjectUtils.notEqual(a, b)).isTrue();
Следует отметить, что ObjectUtils имеет метод equals () . Однако это устарело с Java 7, когда появились Objects#equals
7.2. Метод сравнения объектов#
Теперь давайте сравним порядок объектов с методом ObjectUtils#compare . Это универсальный метод, который принимает два Сопоставимых аргумента этого универсального типа и возвращает Целое число .
Давайте еще раз посмотрим, как использовать Strings :
String first = new String("Hello!"); String second = new String("How are you?"); assertThat(ObjectUtils.compare(first, second)).isNegative();
По умолчанию метод обрабатывает значения null , считая их большими. Он предлагает перегруженную версию, которая предлагает инвертировать это поведение и считать их меньшими, принимая аргумент boolean .
8. Гуава
Теперь давайте взглянем на Гуава . Прежде всего, давайте импортируем зависимость :
com.google.guava guava 29.0-jre
8.1. Объекты#равный метод
Подобно библиотеке Apache Commons, Google предоставляет нам метод определения равенства двух объектов, Объекты#равны . Хотя они имеют разные реализации, они возвращают одни и те же результаты:
String a = new String("Hello!"); String b = new String("Hello!"); assertThat(Objects.equal(a, b)).isTrue();
Хотя он не помечен как устаревший, JavaDoc этого метода говорит, что его следует считать устаревшим, поскольку Java 7 предоставляет метод Objects#equals .
8.2. Методы сравнения
Теперь библиотека Guava не предлагает метод для сравнения двух объектов (мы увидим в следующем разделе, что мы можем сделать для этого), но она предоставляет нам методы для сравнения примитивных значений . Давайте возьмем вспомогательный класс Ints и посмотрим, как работает его метод compare() :
assertThat(Ints.compare(1, 2)).isNegative();
Как обычно, он возвращает целое число , которое может быть отрицательным, нулевым или положительным, если первый аргумент меньше, равен или больше второго, соответственно. Аналогичные методы существуют для всех примитивных типов, за исключением bytes .
8.3. Класс сравнительной цепи
Наконец, библиотека Guava предлагает класс ComparisonChain , который позволяет нам сравнивать два объекта с помощью цепочки сравнений. Мы можем легко сравнить два Человека объекта по имени и фамилии:
Person natalie = new Person("Natalie", "Portman"); Person joe = new Person("Joe", "Portman"); int comparisonResult = ComparisonChain.start() .compare(natalie.getLastName(), joe.getLastName()) .compare(natalie.getFirstName(), joe.getFirstName()) .result(); assertThat(comparisonResult).isPositive();
Базовое сравнение достигается с помощью метода compareTo () , поэтому аргументы, передаваемые методам compare () , должны быть либо примитивами, либо Сопоставимыми s.
9. Заключение
В этой статье мы рассмотрели различные способы сравнения объектов в Java. Мы рассмотрели разницу между одинаковостью, равенством и упорядоченностью. Мы также рассмотрели соответствующие функции в библиотеках Apache Commons и Guava.
Как обычно, полный код этой статьи можно найти на GitHub .