== или .equals(): частая ловушка

Классика собеседований: «Почему new String("a") == new String("a") — false, а Integer a = 100; Integer b = 100; a == b — true?» Если знаешь ответ — ты понимаешь, как Java хранит объекты в памяти.

Оператор == для объектов в Java сравнивает не содержимое, а ссылки — то есть отвечает на вопрос «это один и тот же объект в памяти?», а не «одинаковые ли у них значения?».

Вопрос-крючок

String s1 = new String("a");
String s2 = new String("a");
System.out.println(s1 == s2);          // ?
System.out.println(s1.equals(s2));     // ?

String s3 = "hello";
String s4 = "hello";
System.out.println(s3 == s4);          // ?

Вывод:

false
true
true

Первое сравнение — false. Ключевое слово new явно говорит Java: «создай новый объект в куче», и так происходит дважды — получаются два разных объекта с одинаковым содержимым "a", но по разным адресам в памяти. == сравнивает именно адреса, поэтому получаем false, хотя строки выглядят одинаково.

Второе сравнение — true, потому что .equals() у String переопределён так, чтобы сравнивать посимвольно содержимое, а не адреса. Именно поэтому для сравнения содержимого объектов почти всегда нужен .equals(), а не ==.

Третье сравнение — снова true, и вот тут начинается интересное: s3 и s4 объявлены без new, просто через литералы "hello". И оказываются одним и тем же объектом.

Пул строк: почему литералы совпадают

Java хранит строковые литералы в специальной области памяти — пуле строк (String Pool). Когда компилятор встречает "hello", он сначала проверяет: а нет ли уже такой строки в пуле? Если есть — переиспользует существующий объект. Если нет — создаёт новый и кладёт его в пул. Поэтому s3 = "hello" и s4 = "hello" оба указывают на один и тот же объект в пуле — экономия памяти для одинаковых строк, которых в типичной программе очень много (например, названия полей, повторяющиеся сообщения).

А new String("a") нарочно обходит этот механизм: слово new заставляет Java создать новый объект в обычной куче, вне пула, даже если такая строка там уже есть. Отсюда и разница в поведении.

А что с числами? Кеш Integer от -128 до 127

Integer a = 100;
Integer b = 100;
System.out.println(a == b);      // ?

Integer c = 200;
Integer d = 200;
System.out.println(c == d);      // ?

Вывод:

true
false

Это один из самых коварных вопросов на собеседовании, потому что результат зависит от конкретного числа. У класса Integer есть встроенный кеш: для значений от -128 до 127 Java заранее создаёт объекты Integer и переиспользует их при автоупаковке — примерно как со строковым пулом. Поэтому Integer a = 100 и Integer b = 100 — это один и тот же закешированный объект, и == даёт true.

А для 200 — числа вне диапазона кеша — каждый раз создаётся новый объект Integer, поэтому c == d — уже false, хотя код выглядит абсолютно так же, только число другое. Это именно та ловушка, из-за которой на реальных проектах случаются баги: код прекрасно работает и проходит тесты с маленькими числами, а потом ломается на продакшене на значении вроде 300.

Как сравнивать правильно

// Для объектов (String, Integer, свои классы) — всегда .equals()
if (s1.equals(s2)) { ... }
if (a.equals(b)) { ... }

// Для примитивов (int, double, boolean) — можно и нужно ==
int x = 5, y = 5;
if (x == y) { ... }   // корректно, сравниваются значения

Правило простое: == сравнивает адреса для объектов и значения для примитивов; .equals() нужен всегда, когда важно содержимое объекта, а не то, один ли это экземпляр в памяти. Для String и Integer это правило особенно важно запомнить, потому что кеш и пул строк создают иллюзию, что == «обычно работает» — а потом код ломается на граничных значениях.

Частые ошибки на собеседовании

Сравнивают строки через == и удивляются, что иногда работает, а иногда нет — потому что зависит от того, литерал это или new String().

Не могут объяснить границы кеша Integer. Диапазон -128..127 выбран не случайно — это самые часто используемые в программах небольшие числа, и кеширование их экономит память без большого расхода ресурсов на сам кеш.

Путают равенство ссылок и равенство значений в общем — не только для строк и чисел, но и для своих собственных классов, где по умолчанию .equals() без переопределения ведёт себя так же, как == (об этом — в следующем уроке).

Итоги-шпаргалка

  • == для объектов сравнивает ссылки (адреса в памяти), .equals() — содержимое.
  • Строковые литералы ("hello") берутся из пула строк и могут совпадать по ссылке; new String(...) всегда создаёт новый объект вне пула.
  • Integer кеширует объекты для значений от -128 до 127 — внутри диапазона == может дать true, вне диапазона — почти всегда false.
  • Для сравнения содержимого объектов используй .equals(), не полагайся на ==.
Проверьте себя
1. Integer a = 100; Integer b = 100; System.out.println(a == b); — что выведется и почему?
Afalse, потому что это разные объекты
Btrue, потому что 100 попадает в диапазон кеша Integer (-128..127)
CОшибка компиляции
Dtrue, но только если явно вызвать Integer.valueOf()
2. Почему new String("a") == new String("a") даёт false?
AСтроки в Java вообще нельзя сравнивать через ==
Bnew всегда создаёт новый объект в куче, минуя пул строк, поэтому получаются два разных объекта
CЭто опечатка — на самом деле результат true
DПотому что строка состоит из одного символа