Разберите, почему выражения a == b и a == c дают разные результаты в программе ниже, хотя все строки содержат одинаковый текст.
public class Main {
public static void main(String[] args) {
String a = "java";
String b = "ja" + "va";
String c = new String("java");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a.equals(c));
}
}
Программа выведет true, false, true. Оператор == сравнивает ссылки на объекты, а equals у String сравнивает содержимое строк.
Строковые литералы помещаются в специальный пул строк. a и результат конкатенации двух строковых констант b обозначают одну каноническую строку из пула, тогда как new String("java") создаёт отдельный объект.
Пул строк появился как механизм повторного использования неизменяемых строковых объектов. Строковые литералы встречаются в программах часто, а неизменяемость String позволяет безопасно разделять один объект между разными участками кода.
Такой подход уменьшает количество одинаковых объектов и расход памяти, но не меняет семантику ==: этот оператор по-прежнему проверяет идентичность ссылок, а не равенство текста.
Если сравнивать строки через ==, результат может зависеть от способа их создания. Литералы и некоторые константные выражения могут ссылаться на объект из пула, а строки, полученные во время выполнения или созданные через new, обычно являются другими объектами.
Из-за этого проверка может случайно проходить в тестах с литералами и завершаться ошибкой в рабочем коде, где значение пришло из файла, сети, базы данных или было собрано динамически. Для сравнения содержимого следует использовать equals, обычно вызывая его у заведомо не-null значения либо применяя Objects.equals.
a получает ссылку на строковый литерал "java" из пула. Выражение "ja" + "va" состоит только из строковых литералов, поэтому компилятор может вычислить его как строковую константу; b также ссылается на канонический объект "java". Поэтому a == b равно true.
Вызов new String("java") создаёт новый объект String, даже если его содержимое уже представлено объектом в пуле. Ссылка c отличается от ссылки a, поэтому a == c равно false, но a.equals(c) равно true, поскольку содержимое одинаково.
Метод intern() возвращает каноническое представление строки из пула: после fromInput.intern() результат можно сравнить с литералом через ==. Однако использовать intern() как обычную замену equals не следует: это меняет управление временем жизни строк и может увеличить нагрузку на память и пул.
В веб-приложении разработчик сравнивал код статуса из HTTP-заголовка с литералом через ==. В локальных тестах значение иногда было задано литералом и проверка проходила, но в продакшене оно создавалось парсером входного запроса и имело отдельный объект.
Рассматривались два варианта. Принудительный вызов intern() мог сделать ссылки каноническими, но добавлял лишнюю глобальную работу с пулом и не решал задачу обычного сравнения; переход на equals был проще и не зависел от происхождения строки.
Выбрали "OK".equals(status): литерал не равен null, сравнивается именно содержимое, а решение не требует ручного управления пулом. Ошибки, зависящей от способа создания строки, больше не возникало.
Дополнительный вопрос 1: будет ли результат a == b гарантированно истинным для String a = "ja" + "va";?
Да, в таком случае выражение является константным выражением: оба операнда — строковые литералы, а их конкатенация вычисляется на этапе компиляции. Получившееся значение является строковым литералом и использует пул строк. Это отличается от String b = part1 + part2, где хотя бы одна часть — обычная переменная: конкатенация выполняется во время работы программы.
Дополнительный вопрос 2: что произойдёт при сравнении "java" == new String("java").intern()?
Результат будет true, потому что intern() возвращает ссылку на канонический экземпляр строки из пула, а строковый литерал "java" обращается к тому же каноническому представлению. При этом исходный объект, созданный через new String, не превращается в объект пула и сам по себе не заменяется; используется возвращённая методом ссылка.
Дополнительный вопрос 3: почему вызов text.equals("java") может привести к NullPointerException, а "java".equals(text) — нет?
Если text равен null, вызов экземплярного метода text.equals(...) невозможен, поэтому возникает NullPointerException. Литерал "java" всегда обозначает ненулевой объект, а реализация String.equals корректно вернёт false для null-ссылки. В коде, где обе стороны могут быть null, уместнее использовать Objects.equals(left, right).