== и equals для строк
Переменная типа String, как и переменная-массив, хранит ссылку. У этого есть следствие, которое удивляет почти каждого новичка: две строки с одинаковым текстом сравнивают через == и получают false:
public class Main {
public static void main(String[] args) {
String a = "hi";
String b = "Hi".toLowerCase();
System.out.println(b); // hi
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
}
}В a и b написано одно и то же — hi. Почему == отвечает false, а equals — true?
Вернёмся к карточкам. Представьте две карточки, на которых напечатано одно и то же слово. Про них можно задать два разных вопроса:
- «Это одна и та же карточка?» — так спрашивает
==; - «На карточках написано одно и то же?» — так спрашивает
equals.
Для двух разных карточек с одинаковым словом ответы разные: на первый вопрос — нет, на второй — да.
Что сравнивает ==
Оператор == сравнивает ссылки — так же, как у массивов. Он даёт true, только когда обе переменные ведут к одному объекту, и не смотрит на символы.
В программе выше toLowerCase() создал новую строку hi: это отдельный объект, а a ведёт к другому. Поэтому a == b даёт false, хотя символы совпадают.
Метод equals задаёт второй вопрос: он сравнивает символы двух строк по порядку, как Arrays.equals сравнивает элементы массивов. Ему не важно, один это объект или два.
Пул строк
Почему же == иногда работает? У массивов каждая запись вроде {9, 2, 3} создаёт новый массив, а одинаковые строки-литералы Java не размножает.
Пул строк — общая стойка в памяти, на которой висят строки-литералы, то есть текст в кавычках, написанный прямо в коде. Каждый текст висит на ней один раз, и все одинаковые литералы программы ведут к одной карточке.
Строки, которые создаются во время работы, на стойку не попадают. Так получается строка у toLowerCase(), и так же — у записи new String("hi"). Слово new всегда создаёт новый объект, даже если такой текст на стойке уже висит.
Вот три строчки кода, где встречаются оба случая: String a = "hi";, String b = "hi"; и String c = new String("hi");. Виджет рисует переменные и кучу, как в теме о массивах; пул строк — часть кучи. Выполняйте строчки по одной и следите, к какому объекту ведёт каждая переменная. Потом нажмите на каждое сравнение.
1String a = "hi";2String b = "hi";3String c = new String("hi");
Программа ещё не начала работу. Нажмите кнопку — она выполнит первую строку.
Разберём, что получилось:
a == bдаётtrue: оба литерала"hi"— одна карточка со стойки.a == cдаётfalse:new String("hi")создал вторую карточку, иcведёт к ней.a.equals(c)даётtrue: на обеих карточках написаноhi.
Именно поэтому == опасен. На литералах он отвечает true и кажется рабочим, а на строке, созданной во время работы, молча даёт false. А если методу нечего менять, он может вернуть ту же строку, и тогда "hi".toLowerCase() == "hi" даёт true.
Здесь картинка со стойкой неточна. На настоящую стойку можно повесить две одинаковые карточки. В пуле каждый текст хранится один раз, и это безопасно только потому, что строку никто не может изменить.
Сравнивайте через equals
Правило одно: содержимое строк сравнивают через equals. Для сравнения без учёта регистра букв есть equalsIgnoreCase:
String answer = "YES".toLowerCase();
System.out.println(answer.equals("yes")); // true
System.out.println(answer == "yes"); // false
System.out.println("Yes".equalsIgnoreCase("YES")); // trueanswer.equals("yes")сравнивает символы и даётtrue.answer == "yes"даётfalse: строкаanswerсоздана во время работы, а"yes"— литерал из пула.equalsIgnoreCaseсчитаетYиyодинаковыми и даётtrue.
Чем == отличается от equals — один из самых частых вопросов на собеседовании по Java. Хороший ответ состоит из трёх частей:
==сравнивает ссылки, аequals— содержимое;- одинаковые литералы — один объект из пула, поэтому
==для них даётtrue; new Stringи строки, созданные во время работы, — отдельные объекты, поэтому строки всегда сравнивают черезequals.
Частая ошибка
Команду сравнили с литералом через ==:
String command = "EXIT".toLowerCase();
if (command == "exit") { // ✗
System.out.println("bye");
}Программа ничего не печатает и не сообщает об ошибке. В command лежит exit, но это новый объект, а не литерал из пула, поэтому == даёт false. Компилятор такую запись пропускает. Пишите command.equals("exit").
Главное
==для строк сравнивает ссылки, аequals— символы.- Одинаковые литералы ведут к одному объекту из пула строк, поэтому
==для них даётtrue. new String("hi")и строки, созданные методами во время работы, — новые объекты, и==с литералом даёт для нихfalse.- Содержимое строк сравнивают через
equals, а без учёта регистра — черезequalsIgnoreCase.