В чём разница между == и .equals() в Java?
== сравнивает ссылки (для объектов) или значения (для примитивов); equals() сравнивает логическое содержимое. Переопределяя equals(), всегда переопределяйте hashCode() — иначе HashMap/HashSet работают некорректно.
Оператор == и метод equals()
В Java == всегда сравнивает значения на стеке. Для примитивов (int, long, double) это само значение. Для объектов — адрес ссылки в куче. Метод equals() сравнивает логическое содержимое объекта согласно переопределённой реализации.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false — разные объекты в куче
System.out.println(a.equals(b)); // true — одинаковое содержимое
// String pool (интернирование)
String c = "hello";
String d = "hello";
System.out.println(c == d); // true — оба указывают на один объект из пула
String pool и Integer cache
Строковые литералы интернируются JVM автоматически: "hello" == "hello" даёт true. Это ловушка: new String("hello") создаёт новый объект в куче. Аналогично для Integer: значения -128…127 кэшируются, поэтому Integer.valueOf(127) == Integer.valueOf(127) — true, а для 128 уже false.
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true (из кэша)
Integer p = 128;
Integer q = 128;
System.out.println(p == q); // false (разные объекты)
System.out.println(p.equals(q)); // true
Контракт equals()
Переопределение equals() должно удовлетворять пяти свойствам из javadoc Object:
- Рефлексивность:
x.equals(x)→true - Симметричность:
x.equals(y)↔y.equals(x) - Транзитивность: если
x.equals(y)иy.equals(z), тоx.equals(z) - Консистентность: повторные вызовы возвращают одно значение при неизменных данных
- Ненулевость:
x.equals(null)→false
Связь с hashCode
Критически важное правило: если a.equals(b), то a.hashCode() == b.hashCode() обязательно. Обратное не требуется (коллизии допустимы). Нарушение этого правила ломает HashMap, HashSet и все hash-коллекции.
public class User {
private final String email;
public User(String email) { this.email = email; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
}
// В record equals/hashCode генерируются автоматически по всем компонентам
record Point(int x, int y) {}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
Подводные камни
- Сравнение строк через
==— самая частая ошибка джунов:userInput == "admin"почти всегдаfalse. - Переопределили
equals(), но забылиhashCode()— объект не найдётся вHashSetпосле добавления. - Нарушение симметричности при наследовании: если
ColorPoint.equals(Point)учитывает цвет, аPoint.equals(ColorPoint)нет — контракт нарушен. instanceofвequals()разрешает подклассы;getClass() == o.getClass()— строгое сравнение. ДляrecordJVM генерирует строгое сравнение.- Mutable поля в
equals()/hashCode()могут сломать HashMap при изменении объекта после вставки. - Autoboxing:
long == Longраспаковывает Long, ноLong == Longсравнивает ссылки — результат неожиданный для больших чисел. Objects.equals(a, b)безопасен к null в отличие отa.equals(b)— предпочитайте его в реализации.
Common mistakes
- Путать термин «equals identity» с соседним механизмом Java.
- Не называть границу lifecycle, transaction, thread или request для «equals identity».
- Игнорировать production-эффекты «equals identity»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «equals identity» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «equals identity».
- Уточнить, какие настройки или API меняют «equals identity» в реальном сервисе.