В чём разница между == и equals() для String
В Java оператор == и метод .equals() решают две принципиально разные задачи:
🟢 Junior Level
В Java оператор == и метод .equals() решают две принципиально разные задачи:
- Оператор
==(проверка идентичности ссылки): Сравнивает адреса в памяти (физические указатели). Он возвращаетtrueтолько в том случае, если обе переменные ссылаются на один и тот же экземпляр объекта в куче. - Метод
.equals()(проверка логической эквивалентности): Сравнивает символьное содержимое двух строк. Он возвращаетtrue, если последовательности символов обеих строк идентичны по длине и значению, независимо от того, где эти строки физически выделены в памяти.
Суть в 30 секундах
Если вам нужно узнать, содержат ли две строки один и тот же текст — всегда используйте equals(). Использование == со строками допустимо практически исключительно для проверки на null (str == null).
String s1 = "Hello"; // Объект в String Pool
String s2 = "Hello"; // Ссылка на тот же объект в String Pool
String s3 = new String("Hello"); // Принудительно новый объект в куче вне пула
System.out.println(s1 == s2); // true (один и тот же адрес в пуле)
System.out.println(s1 == s3); // false (разные адреса в памяти)
System.out.println(s1.equals(s3)); // true (одинаковое текстовое содержимое)
🟡 Middle Level
Внутренний алгоритм метода String.equals()
В современных версиях Java (Java 9+, HotSpot) метод equals класса String реализует строго оптимизированную 5-шаговую проверку:
public boolean equals(Object anObject) {
// Шаг 1: Проверка идентичности ссылок (Fast Path)
if (this == anObject) {
return true;
}
// Шаг 2: Проверка принадлежности к классу String
return (anObject instanceof String aString)
// Шаг 3: Сравнение кодировки (Latin-1 против UTF-16)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
// Шаг 4 и 5: Сравнение длины и побайтовое сравнение массивов
&& StringLatin1.equals(value, aString.value);
}
this == anObject(Fast Path): Если обе переменные указывают на один и тот же адрес (например, интернированные строки из пула), метод возвращаетtrueмгновенно за $O(1)$, не сравнивая массивы байтов.instanceof String: Защищает от передачи объектов других типов и отnull(если аргументnull,instanceofбезопасно вернетfalse).this.coder == aString.coder: Если одна строка состоит только из Latin-1 символов (coder = 0), а другая содержит UTF-16 символы (coder = 1), они гарантированно не равны. Проверка длины и байтов даже не запускается.- Сравнение длины: Если массивы
valueимеют разную длину — немедленный возвратfalse. - Векторизованное сравнение байтов: Побайтовое сравнение массивов
byte[].
Сравнительная таблица
| Критерий | Оператор == | Метод equals() |
| :— | :— | :— |
| Что проверяет | Идентичность ссылок (Memory Address) | Эквивалентность содержимого (State / Value) |
| Временная сложность | Всегда $O(1)$ (1 машинная инструкция) | $O(1)$ для одинаковых ссылок, $O(N)$ для сравнения символов |
| Поведение при null слева | Безопасно (null == s2 вернет false) | NullPointerException (null.equals(s2)) |
| Поведение при null справа | Безопасно (s1 == null) | Безопасно возвращает false (s1.equals(null)) |
| Строковые литералы | Возвращает true из-за String Pool | Возвращает true |
| Динамические строки (DB/JSON) | Возвращает false | Возвращает true |
Типичные ошибки и грабли
- Вызов метода на потенциальном
null:// ПЛОХО: выбросит NullPointerException, если status == null if (status.equals("ACTIVE")) { ... } // ХОРОШО: константа слева гарантирует null-safety if ("ACTIVE".equals(status)) { ... } // ОТЛИЧНО: универсальный метод из java.util.Objects if (Objects.equals(status, "ACTIVE")) { ... } - Сравнение
StringиStringBuilderчерезequals():"test".equals(new StringBuilder("test"))всегда вернетfalse, так как аргумент не проходит проверкуinstanceof String! Для сравнения разных реализацийCharSequenceнужно использовать методcontentEquals().
🔴 Senior Level
Аппаратная оптимизация: SIMD-векторизация C2 JIT
Методы StringLatin1.equals и StringUTF16.equals помечены аннотацией @IntrinsicCandidate.
В процессе C2 JIT-компиляции HotSpot заменяет цикл побайтового сравнения на низкоуровневый ассемблерный intrinsic vectorizedMismatch:
- Вместо побайтового перебора в цикле процессор загружает векторные регистры размером 128, 256 или 512 бит (SSE4.2, AVX-2, AVX-512 на архитектуре x86-64 или NEON на ARM64).
- Инструкция
VPXOR(векторный исключающий ИЛИ) сравнивает 32 или 64 символа одновременно за один такт процессора. - Если векторный XOR дает нулевой результат, все 32/64 байта идентичны, и процессор переходит к следующему блоку.
- Если обнаружено несовпадение, инструкция
VPMOVMSKBформирует битовую маску, позволяющую за 1 инструкцию определить точный индекс несовпавшего байта. Благодаря этому сравнение даже строк длиной в сотни символов занимает единицы наносекунд.
Атаки по времени (Timing Attacks) и Constant-Time сравнение
Стандартный метод String.equals() оптимизирован для максимальной скорости: он немедленно прерывает выполнение (Early Return), как только встречает первый несовпадающий символ.
Уязвимость в безопасности:
Если вы проверяете секретные токены, API-ключи, цифровые подписи или пароли через String.equals(), злоумышленник может измерять задержку ответа сервера с точностью до наносекунд:
- Если первый байт токена неверен, метод вернет
falseза ~1 нс. - Если первые 15 байт верны, а ошибочен 16-й байт, метод проработает дольше (~4 нс). Это позволяет злоумышленнику подобрать секретный токен посимвольно (Side-Channel Timing Attack).
Защита: В криптографии и безопасности паролей/токенов сравнение обязано выполняться за константное время (Constant-Time), независимо от позиции первой ошибки:
// Защита от timing attacks через MessageDigest.isEqual
byte[] secretExpected = expectedToken.getBytes(StandardCharsets.UTF_8);
byte[] userProvided = inputToken.getBytes(StandardCharsets.UTF_8);
boolean isValid = MessageDigest.isEqual(secretExpected, userProvided);
Внутри MessageDigest.isEqual используется побитовый оператор | без условных ветвлений, что гарантирует одинаковое время выполнения для любых входных данных.
Контракт equals() и hashCode()
Класс String строго выполняет фундаментальный контракт java.lang.Object:
- Если
s1.equals(s2) == true, тоs1.hashCode() == s2.hashCode()обязан быть строго равен. - В классе
Stringхеш-код вычисляется по детерминированной формуле: \(s[0] \cdot 31^{n-1} + s[1] \cdot 31^{n-2} + \dots + s[n-1]\) - Значение хеш-кода кэшируется в приватном поле
private int hashпосле первого вызова. - Разные по содержимому строки (
equals() == false) могут случайно иметь одинаковый хеш-код (хеш-коллизия, например,"FB"и"Ea").
4 Tricky Questions
1. Почему выражение "abc".equals(new StringBuilder("abc")) успешно компилируется, но всегда возвращает false? Какой метод использовать для правильного сравнения?
Ответ:
Метод equals(Object anObject) в классе String принимает любой объект Object, поэтому выражение синтаксически корректно и компилируется без предупреждений.
Однако внутри метода первой проверкой после ссылки идет: if (anObject instanceof String). Поскольку StringBuilder не является наследником String (они оба наследники Object и реализуют общий интерфейс CharSequence), проверка instanceof возвращает false.
Для логического сравнения содержимого строки с любыми другими реализациями CharSequence (StringBuilder, StringBuffer, CharBuffer) класс String предоставляет специализированный метод:
String str = "abc";
StringBuilder sb = new StringBuilder("abc");
System.out.println(str.equals(sb)); // false (всегда!)
System.out.println(str.contentEquals(sb)); // true (посимвольное сравнение CharSequence)
2. Гарантирует ли равенство хеш-кодов s1.hashCode() == s2.hashCode(), что s1.equals(s2) вернет true?
Ответ:
Нет, категорически не гарантирует.
Хеш-код строки — это 32-битное целое число (int), которое имеет всего $2^{32} \approx 4.29 \times 10^9$ возможных значений. Количество же возможных строковых комбинаций бесконечно. В силу принципа Дирихле неизбежны хеш-коллизии (разные строки с одинаковым хешем).
Классический пример коллизии в Java:
String s1 = "FB";
String s2 = "Ea";
System.out.println(s1.hashCode() == s2.hashCode()); // true (у обоих hash = 2236)
System.out.println(s1.equals(s2)); // false ("FB" != "Ea")
Однако обратное утверждение строго истинно: если строки равны по equals(), их хеш-коды гарантированно совпадают.
3. Чем отличается s1.hashCode() от System.identityHashCode(s1) и как это соотносится с == и equals()?
Ответ:
s1.hashCode(): Вычисляет хеш-код на основе значений символов (содержимого строки). Две разные строки с одинаковым текстом дадут одинаковыйhashCode(). Это основа работыHashMapиHashSet.System.identityHashCode(s1): Возвращает хеш-код идентичности объекта, генерируемый HotSpot JVM на основе адреса объекта в памяти или значения из заголовка Mark Word (так, как если бы методhashCode()не был переопределен в классеString).- Если
s1 == s2(один и тот же объект), тоSystem.identityHashCode(s1) == System.identityHashCode(s2). - Если
s1иs2созданы черезnew String("test"), тоs1.hashCode() == s2.hashCode()вернетtrue, ноSystem.identityHashCode(s1) == System.identityHashCode(s2)с вероятностью 99.999% вернетfalse.
- Если
4. В каком случае метод String.equals() может вернуть false еще до проверки длины и содержимого строки в Java 9+?
Ответ:
В случае несовпадения поля coder (флага кодировки Compact Strings).
Начиная с Java 9, строка хранит массив byte[] value и однобайтовое поле coder:
coder = 0(LATIN1): все символы укладываются в диапазон ISO-8859-1 (1 байт на символ).coder = 1(UTF16): хотя бы один символ требует 2 байта (кириллица, китайские иероглифы, эмодзи). Если сравниваются две строки, у одной из которыхcoder == 0, а у второйcoder == 1, HotSpot JVM немедленно возвращаетfalse, не выполняя проверку длины массива и побайтовое сравнение, так как строки в разных кодировках с разным набором допустимых символов не могут быть эквивалентны.
🎯 Шпаргалка для интервью
30-секундный ответ
«Оператор
==сравнивает физические адреса объектов в памяти (идентичность ссылок), возвращаяtrue, только если обе переменные указывают на один и тот же объект. Метод.equals()сравнивает фактическое логическое содержимое строк (значения символов). Для сравнения содержимого строк всегда используетсяequals()илиObjects.equals(). Оператор==используется со строками только для проверки наnull».
Ключевые тезисы для Senior-интервью
- 5 шагов
String.equals():==fast-path $\to$instanceof String$\to$ проверкаcoder(Compact Strings) $\to$ проверкаlength$\to$ SIMD-векторизованное сравнение байтов. - SIMD Intrinsic: C2 компилятор заменяет посимвольный цикл на векторные инструкции (AVX-512 / AVX2), сравнивая до 64 байт за такт.
- Безопасность и Timing Attacks:
equals()уязвим к тайминг-атакам из-за раннего выхода. Для токенов и паролей используетсяMessageDigest.isEqual()(constant-time comparison). contentEquals(): Единственный корректный способ сравнитьStringсоStringBuilderилиStringBuffer.- Контракт
equalsиhashCode: Равенство поequals()влечет равенствоhashCode(), но не наоборот (коллизии).
Красные флаги (Чего говорить категорически нельзя)
- ❌ Утверждать, что «
==можно использовать для коротких строк, аequals()— для длинных». - ❌ Сравнивать
StringсоStringBuilderчерезstr.equals(sb)(всегда возвращаетfalse). - ❌ Забывать про
NullPointerExceptionпри вызовеvar.equals("CONST")вместо"CONST".equals(var). - ❌ Считать, что совпадение
hashCode()гарантирует равенство строк.