Перейти к содержимому
Теория · Строки · урок 5 из 11

== и equals для строк

Переменная типа String, как и переменная-массив, хранит ссылку. У этого есть следствие, которое удивляет почти каждого новичка: две строки с одинаковым текстом сравнивают через == и получают false:

JAVA
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");. Виджет рисует переменные и кучу, как в теме о массивах; пул строк — часть кучи. Выполняйте строчки по одной и следите, к какому объекту ведёт каждая переменная. Потом нажмите на каждое сравнение.

ВживуюЛитералы и new Stringпеременных: 0 · объектов: 0
номера объектов условные

Программа ещё не начала работу. Нажмите кнопку — она выполнит первую строку.

Выполните три строчки, затем нажмите на каждое сравнение

Разберём, что получилось:

  1. a == b даёт true: оба литерала "hi" — одна карточка со стойки.
  2. a == c даёт false: new String("hi") создал вторую карточку, и c ведёт к ней.
  3. a.equals(c) даёт true: на обеих карточках написано hi.

Именно поэтому == опасен. На литералах он отвечает true и кажется рабочим, а на строке, созданной во время работы, молча даёт false. А если методу нечего менять, он может вернуть ту же строку, и тогда "hi".toLowerCase() == "hi" даёт true.

Здесь картинка со стойкой неточна. На настоящую стойку можно повесить две одинаковые карточки. В пуле каждый текст хранится один раз, и это безопасно только потому, что строку никто не может изменить.

Сравнивайте через equals

Правило одно: содержимое строк сравнивают через equals. Для сравнения без учёта регистра букв есть equalsIgnoreCase:

JAVA
String answer = "YES".toLowerCase();
System.out.println(answer.equals("yes"));          // true
System.out.println(answer == "yes");               // false
System.out.println("Yes".equalsIgnoreCase("YES")); // true
  1. answer.equals("yes") сравнивает символы и даёт true.
  2. answer == "yes" даёт false: строка answer создана во время работы, а "yes" — литерал из пула.
  3. equalsIgnoreCase считает Y и y одинаковыми и даёт true.

Чем == отличается от equals — один из самых частых вопросов на собеседовании по Java. Хороший ответ состоит из трёх частей:

  • == сравнивает ссылки, а equals — содержимое;
  • одинаковые литералы — один объект из пула, поэтому == для них даёт true;
  • new String и строки, созданные во время работы, — отдельные объекты, поэтому строки всегда сравнивают через equals.

Частая ошибка

Команду сравнили с литералом через ==:

JAVA
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.