1. Обзор
В этом руководстве мы опишем две основные проверки равенства в Java — равенство ссылок и равенство значений. Мы сравним их, покажем примеры и выделим ключевые различия между ними.
Кроме того, мы сосредоточимся на проверках null
и поймем, почему мы должны использовать равенство ссылок вместо равенства значений при работе с объектами.
2. Эталонное равенство
Мы начнем с понимания сравнения ссылок, которое представлено оператором равенства ( ==
). Равенство ссылок возникает, когда две ссылки указывают на один и тот же объект в памяти.
2.1. Оператор равенства с примитивными типами
Мы знаем, что примитивные типы в Java — это простые необработанные значения, не относящиеся к классу. Когда мы используем оператор равенства с примитивными типами, мы просто сравниваем их значения:
int a = 10;
int b = 15;
assertFalse(a == b);
int c = 10;
assertTrue(a == c);
int d = a;
assertTrue(a == d);
Как показано выше, проверка на равенство и ссылка работают одинаково для примитивов . Когда мы инициализируем новый примитив с тем же значением, проверка возвращает true.
Более того, оператор возвращает тот же результат, если мы переназначаем исходное значение новой переменной и сравниваем ее.
Давайте теперь выполним нулевые
проверки:
int e = null; // compilation error
assertFalse(a == null); // compilation error
assertFalse(10 == null); // compilation error
Java запрещает присваивать значение null
примитиву. Как правило, мы не можем выполнять какие -либо проверки на null
с помощью оператора равенства для примитивных переменных или значений.
2.2. Оператор равенства с типами объектов
Что касается типов объектов в Java , оператор равенства выполняет только ссылочное сравнение на равенство , игнорируя значения объекта. Прежде чем мы реализуем тесты, давайте создадим простой пользовательский класс:
public class Person {
private String name;
private int age;
// constructor, getters, setters...
}
Теперь давайте инициализируем некоторые объекты класса и проверим результаты оператора равенства:
Person a = new Person("Bob", 20);
Person b = new Person("Mike", 40);
assertFalse(a == b);
Person c = new Person("Bob", 20);
assertFalse(a == c);
Person d = a;
assertTrue(a == d);
Результаты совсем другие, чем раньше. Вторая проверка возвращает false
, тогда как для примитивов мы получили true .
Как мы упоминали ранее, оператор равенства игнорирует внутренние значения объекта при сравнении. Он только проверяет, ссылаются ли две переменные на один и тот же адрес памяти .
В отличие от примитивов, мы можем использовать null
при работе с объектами:
assertFalse(a == null); Person e = null; assertTrue(e == null);
Используя оператор равенства и сравнивая null,
мы проверяем, не инициализирован ли уже объект, присвоенный переменной .
3. Цените равенство
Давайте теперь сосредоточимся на проверке равенства значений. Равенство значений имеет место, когда два отдельных объекта имеют одинаковые значения или состояние.
Это сравнивает значения и тесно связано с методом equals()
объекта . Как и прежде, давайте сравним его использование с примитивами и объектными типами, обратив внимание на ключевые отличия.
3.1. Метод equals()
с примитивными типами
Как мы знаем, примитивы являются базовыми типами с одним значением и не реализуют никаких методов. Поэтому невозможно напрямую вызвать метод equals()
с помощью примитивов :
int a = 10; assertTrue(a.equals(10)); // compilation error
Однако, поскольку у каждого примитива есть свой собственный класс-оболочка , мы можем использовать механизм упаковки,
чтобы преобразовать его в его объектное представление. Затем мы можем легко вызвать метод equals()
, как если бы мы использовали типы объектов:
int a = 10;
Integer b = a;
assertTrue(b.equals(10));
3.2. Метод equals()
с типами объектов
Вернемся к нашему классу Person .
Чтобы метод equals()
работал правильно, нам нужно переопределить метод в пользовательском классе, учитывая поля, содержащиеся в классе:
public class Person {
// other fields and methods omitted
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
}
Во-первых, метод equals()
возвращает true
, если заданное значение имеет ту же ссылку, что и проверяется оператором ссылки. Если нет, начинаем проверку на равенство.
Далее мы проверяем равенство объектов Class
для обоих значений. Мы возвращаем false
, если они разные. В противном случае продолжаем проверку на равенство. Наконец, мы возвращаем объединенный результат сравнения каждого свойства по отдельности.
Теперь давайте изменим предыдущий тест и проверим результаты:
Person a = new Person("Bob", 20);
Person b = new Person("Mike", 40);
assertFalse(a.equals(b));
Person c = new Person("Bob", 20);
assertTrue(a.equals(c));
Person d = a;
assertTrue(a.equals(d));
Как мы видим, вторая проверка возвращает true
, а не ссылочное равенство. Наш переопределенный метод equals()
сравнивает внутренние значения объектов.
Если мы не переопределяем метод equals()
, используется метод из родительского класса Object
. Поскольку метод Object.equals()
выполняет только проверку равенства ссылок, поведение может отличаться от ожидаемого при сравнении объектов Person .
Хотя выше мы не показывали метод hashCode()
, следует отметить, что важно переопределять его всякий раз, когда мы переопределяем метод equals () ,
чтобы обеспечить согласованность между этими методами . ``
4. Нулевое равенство
Напоследок проверим, как работает метод equals()
с нулевым
значением:
Person a = new Person("Bob", 20);
Person e = null;
assertFalse(a.equals(e));
assertThrows(NullPointerException.class, () -> e.equals(a));
Когда мы проверяем его с помощью метода equals()
на другом объекте, мы получаем два разных результата в зависимости от порядка этих переменных. Последний оператор вызывает исключение, потому что мы вызываем метод equals()
для нулевой
ссылки. Чтобы исправить последнее утверждение, мы должны сначала вызвать проверку оператора равенства:
assertFalse(e != null && e.equals(a));
Теперь левая часть условия возвращает false
, что делает весь оператор равным false
, предотвращая создание исключения NullPointerException
. Поэтому мы должны не забыть сначала проверить, что значение, для которого мы вызываем метод equals()
, не равно null
, иначе это может привести к досадным ошибкам.
Более того, начиная с Java 7, мы можем использовать статический метод
Objects#equals()
, безопасный для нулей, для проверки на равенство: ``
assertFalse(Objects.equals(e, a));
assertTrue(Objects.equals(null, e));
Этот вспомогательный метод выполняет дополнительные проверки, чтобы предотвратить создание исключения NullPointerException
, возвращая значение true
, если оба параметра имеют значение null
.
5. Вывод
В этой статье мы обсудили проверки равенства ссылок и значений для `` примитивных и объектных значений.
Чтобы проверить ссылочное равенство, мы используем оператор == .
Этот оператор работает несколько иначе для примитивных значений и объектов. Когда мы используем оператор равенства с примитивами, он сравнивает значения. С другой стороны, когда мы используем его для объектов, он проверяет ссылки на память. Сравнивая его с нулевым
значением, мы просто проверяем, что объект инициализирован в памяти.
Чтобы выполнить проверку равенства значений в Java, мы используем метод equals()
, унаследованный от Object
. Примитивы — это простые значения, не относящиеся к классу, поэтому этот метод нельзя вызывать без переноса.
Нам также нужно помнить, что метод equals()
следует вызывать только для созданного объекта. В противном случае будет выброшено исключение. Чтобы предотвратить это, если мы подозреваем нулевое
значение, мы должны проверить значение с помощью оператора == .
Как всегда, исходный код примеров доступен на GitHub .