1. Введение
Сравнение объектов является важной функцией объектно-ориентированных языков программирования.
В этом руководстве мы рассмотрим некоторые функции языка Java, которые позволяют нам сравнивать объекты. Мы также рассмотрим такие функции во внешних библиотеках.
2. ==
и !=
Операторы
Начнем с операторов ==
и !=
, которые могут определить, являются ли два объекта Java одинаковыми или нет соответственно.
2.1. примитивы
Для примитивных типов быть одинаковым означает иметь одинаковые значения:
assertThat(1 == 1).isTrue();
Благодаря автоматической распаковке это также работает при сравнении примитивного значения с его эквивалентом типа-оболочки :
Integer a = new Integer(1);
assertThat(1 == a).isTrue();
Если два целых числа имеют разные значения, оператор ==
вернет false
, а оператор !=
вернет true
.
2.2. Объекты
Допустим, мы хотим сравнить два типа оболочки Integer с одинаковым значением:
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#valueOf :
Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(1);
assertThat(a == b).isTrue();
В этом случае они считаются одинаковыми. Это связано с тем, что метод valueOf()
сохраняет Integer
в кэше, чтобы избежать создания слишком большого количества объектов-оболочек с одинаковым значением. Поэтому метод возвращает один и тот же экземпляр Integer
для обоих вызовов.
Java также делает это для String
:
assertThat("Hello!" == "Hello!").isTrue();
Однако, если они созданы с помощью нового
оператора, они не будут одинаковыми.
Наконец, две нулевые
ссылки считаются одинаковыми, в то время как любой ненулевой
объект считается отличным от нулевого
:
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
, когда оба объекта одинаковы.
Следует отметить, что мы можем передать нулевой
объект в качестве аргумента метода, но не в качестве объекта, для которого мы вызываем метод.
Мы также можем использовать метод equals()
с нашим собственным объектом. Допустим, у нас есть класс Person :
public class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Мы можем переопределить метод equals()
для этого класса, чтобы мы могли сравнивать два Person
на основе их внутренних данных:
@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
Теперь давайте посмотрим на статический метод Objects#equals
. Мы упоминали ранее, что мы не можем использовать null
в качестве значения первого объекта, иначе будет выброшено исключение NullPointerException .
Метод equals()
вспомогательного класса Objects
решает эту проблему. Он принимает два аргумента и сравнивает их, а также обрабатывает нулевые
значения.
Давайте снова сравним объекты Person :
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,
он вернет true
, а если только один из них равен null
, он вернет false
.
Это может быть очень удобно. Допустим, мы хотим добавить необязательную дату рождения в наш класс Person :
public Person(String firstName, String lastName, LocalDate birthDate) {
this(firstName, lastName);
this.birthDate = birthDate;
}
Затем нам нужно обновить наш метод equals() , но с
нулевой
обработкой. Мы можем сделать это, добавив условие в наш метод equals() :
birthDate == null ? that.birthDate == null : birthDate.equals(that.birthDate);
Однако, если мы добавим в наш класс слишком много полей, допускающих значение NULL, он может стать действительно запутанным. Использование метода Objects#equals
в нашей реализации equals()
намного чище и улучшает читаемость:
Objects.equals(birthDate, that.birthDate);
5. Сопоставимый
интерфейс
Логику сравнения также можно использовать для размещения объектов в определенном порядке. Интерфейс Comparable
позволяет нам определять порядок между объектами , определяя, является ли объект больше, равен или меньше другого.
Интерфейс Comparable
является универсальным и имеет только один метод, compareTo()
, который принимает аргумент универсального типа и возвращает значение int
. Возвращаемое значение отрицательное, если оно
меньше аргумента, 0, если они равны, и положительное в противном случае.
Допустим, в нашем классе Person
мы хотим сравнить объекты Person
по их фамилии:
public class Person implements Comparable<Person> {
//...
@Override
public int compareTo(Person o) {
return this.lastName.compareTo(o.lastName);
}
}
Метод compareTo()
вернет отрицательное целое
число , если вызывается с Person
, фамилия которого больше, чем this
, ноль, если та же фамилия, и положительное значение в противном случае.
Для получения дополнительной информации взгляните на нашу статью на эту тему .
6.
Интерфейс компаратора
Интерфейс Comparator
является универсальным и имеет метод сравнения
, который принимает два аргумента этого универсального типа и возвращает целое число
. Мы уже видели этот шаблон ранее с интерфейсом Comparable
.
Компаратор
аналогичен; однако он отделен от определения класса. Следовательно, мы можем определить столько компараторов
, сколько захотим для класса, где мы можем предоставить только одну реализацию Comparable
.
Давайте представим, что у нас есть веб-страница, отображающая людей в виде таблицы, и мы хотим предложить пользователю возможность сортировать их по именам, а не по фамилиям. Это невозможно с Comparable
, если мы также хотим сохранить нашу текущую реализацию, но мы можем реализовать наши собственные Comparators
.
Давайте создадим компаратор
людей
, который будет сравнивать их только по именам:
Comparator<Person> compareByFirstNames = Comparator.comparing(Person::getFirstName);
Теперь давайте отсортируем список
людей, используя этот компаратор
:
Person joe = new Person("Joe", "Portman");
Person allan = new Person("Allan", "Dale");
List<Person> people = 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 . Прежде всего, давайте импортируем зависимость Maven :
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
7.1.
Метод ObjectUtils#notEqual
Сначала поговорим о методе ObjectUtils#notEqual
. Требуется два аргумента Object
, чтобы определить, не равны ли они, в соответствии с их собственной реализацией метода equals() .
Он также обрабатывает нулевые
значения.
Давайте повторно используем наши примеры 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
Теперь давайте сравним порядок объектов с помощью метода ObjectUtils#compare
. Это универсальный метод, который принимает два аргумента Comparable
этого универсального типа и возвращает Integer
.
Давайте снова посмотрим на это, используя Strings
:
String first = new String("Hello!");
String second = new String("How are you?");
assertThat(ObjectUtils.compare(first, second)).isNegative();
По умолчанию метод обрабатывает нулевые
значения, считая их большими. Он также предлагает перегруженную версию, которая предлагает инвертировать это поведение и считать его меньшим, принимая логический
аргумент.
8. Гуава
Давайте посмотрим на Гуаву . Прежде всего, давайте импортируем зависимость :
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
8.1. Объекты#равный
метод
Подобно библиотеке Apache Commons, Google предоставляет нам метод определения равенства двух объектов, Objects#equal
. Хотя они имеют разные реализации, они возвращают одинаковые результаты:
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.
Класс ComparisonChain
Наконец, библиотека Guava предлагает класс ComparisonChain
, который позволяет нам сравнивать два объекта по цепочке сравнений. Мы можем легко сравнить два объекта Person
по имени и фамилии:
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()
, должны быть либо примитивами, либо Comparable
s.
9. Заключение
В этой статье мы узнали о различных способах сравнения объектов в Java. Мы рассмотрели разницу между одинаковостью, равенством и порядком. Мы также рассмотрели соответствующие функции в библиотеках Apache Commons и Guava.
Как обычно, полный код этой статьи можно найти на GitHub .