Вопрос 10 · Раздел 12

В чём разница между == и equals() для String?

Алгоритм String.equals():

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

== сравнивает адреса в памяти (является ли это одним и тем же объектом).

equals() сравнивает содержимое (одинаковый ли текст внутри).

Пример:

String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");

System.out.println(s1 == s2);      // true  — один объект в String Pool
System.out.println(s1 == s3);      // false — s3 отдельный объект
System.out.println(s1.equals(s3)); // true  — текст одинаковый

Правило: Для строк всегда используйте equals(). Оператор == — только для проверки на null.


🟡 Middle Level

Как работает equals()

Алгоритм String.equals():

  1. Сначала проверяет == (fast path — если это один объект, сразу true)
  2. Проверяет, что объект — это String
  3. Сравнивает поле coder (одинаковая ли кодировка)

coder (Java 9+) — флаг, указывающий, хранится ли строка в Latin1 (1 байт/символ) или UTF-16 (2 байта/символ).

  1. Сравнивает длины
  2. Посимвольно сравнивает массивы byte[]
// Упрощённо
public boolean equals(Object o) {
    if (this == o) return true;           // Fast path
    if (!(o instanceof String)) return false;
    String other = (String) o;
    if (this.value.length != other.value.length) return false;
    return Arrays.equals(this.value, other.value);
}

Типичные ошибки

  1. Ошибка: s1.equals(s2) когда s1 может быть null Решение: "constant".equals(variable) или Objects.equals(s1, s2)

  2. Ошибка: Думать, что == работает для одинаковых литералов всегда Решение: Это работает только для литералов в одном классе. Данные извне — в куче.

Сравнение

| Критерий | == | equals() | | —————- | —————– | ——————————————————————– | | Что сравнивает | Ссылки (identity) | Содержимое (equality) | | Скорость | O(1) | O(1) для identical, O(n) для разных объектов с одинаковым содержимым | | Для литералов | Работает | Работает | | Для new String() | Не работает | Работает | | null-safe | Да (s == null) | Нет (s.equals() бросит NPE) |


🔴 Senior Level

Internal Implementation

Байт-код сравнения:

// == → if_acmpeq (JVM instruction: compare references)
// equals() → invokevirtual java/lang/String.equals

OpenJDK реализация (Java 9+):

public boolean equals(Object anObject) {
    if (this == anObject) return true;
    if (anObject instanceof String anotherString) {
        if (coder == anotherString.coder) {
            return isLatin1()
                ? StringLatin1.equals(value, anotherString.value)
                : StringUTF16.equals(value, anotherString.value);
        }
    }
    return false;
}

// StringLatin1.equals — intrinsic
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
    if (value.length == other.length) {
        for (int i = 0; i < value.length; i++) {
            if (value[i] != other[i]) return false;
        }
        return true;
    }
    return false;
}

@HotSpotIntrinsicCandidate — JVM заменяет Java-код на оптимизированную CPU-инструкцию (SIMD vectorized comparison).

Архитектурные Trade-offs

Identity (==) vs Equality (equals()):

  • == — проверка идентичности объектов (reference equality)
  • equals() — проверка эквивалентности содержимого (logical equality)

Контракт equals():

  • Рефлексивность: x.equals(x) всегда true
  • Симметричность: x.equals(y) == y.equals(x)
  • Транзитивность: если x.equals(y) и y.equals(z), то x.equals(z)
  • Консистентность: повторные вызовы дают тот же результат
  • Для любых x: x.equals(null)false

Edge Cases

  1. Контракт с hashCode(): Если s1.equals(s2)s1.hashCode() == s2.hashCode(). В String это соблюдается — хеш вычисляется из содержимого.

  2. Coder mismatch: String с Latin1 ("abc") и String с UTF-16 ("abc" + Cyrillic) — equals() вернёт false на этапе проверки coder, даже если визуально символы одинаковы.

  3. String deduplication (G1 GC): Объединяет byte[] массивы, но объекты String остаются разными. == вернёт false, equals()true.

Производительность

| Сценарий | == | equals() | | ——————————- | —— | ——————— | | Identical objects | ~0.3ns | ~0.3ns (fast path) | | Same content, different objects | ~0.3ns | ~2-5ns (SIMD) | | Different content, same length | ~0.3ns | ~1-2ns (early exit) | | Different lengths | ~0.3ns | ~0.5ns (length check) |

Production Experience

Сценарий: Auth middleware — проверка API ключа:

// ПЛОХО: == — можно обойти, если хакер угадал адрес
if (apiKey == expectedKey) { grant(); }

// ХОРОШО: equals()
if (expectedKey.equals(apiKey)) { grant(); }

// ЛУЧШЕ: constant-time comparison (защита от timing attacks)
if (MessageDigest.isEqual(expectedKey.getBytes(), apiKey.getBytes())) { grant(); }
// Обратите внимание: getBytes() создаёт новые массивы — это аллокация.
// Для высоконагруженных систем рассмотрите сравнение на уровне byte[] напрямую.

Best Practices для Highload

  • equals() — дефолтный выбор для сравнения содержимого
  • Objects.equals(a, b) — null-safe, делегирует в a.equals(b)
  • "CONSTANT".equals(variable) — null-safe без дополнительных объектов
  • Для security-sensitive сравнения: constant-time algorithms (защита от timing attacks)
  • == — только для null checks и гарантированно интернированных строк

🎯 Шпаргалка для интервью

Обязательно знать:

  • == сравнивает ссылки (адреса в памяти), equals() — содержимое строк
  • equals() имеет fast path: сначала проверяет == (если один объект — сразу true)
  • String.equals() проверяет coder, длину, затем посимвольное сравнение
  • == — O(1), equals() — O(n) где n — длина строки
  • equals() может бросить NPE если вызван на null — используйте "const".equals(var) или Objects.equals()
  • JVM оптимизирует equals() через SIMD-инструкции для коротких строк
  • Для security-sensitive сравнения используйте constant-time comparison

Частые уточняющие вопросы:

  • Можно ли использовать == для строк? — Только для null checks (s == null) или гарантированно интернированных строк. В 99% случаев — нет.
  • Почему equals() быстрее, чем кажется? — Fast path == + SIMD оптимизация + early exit при несовпадении.
  • Что быстрее: == или equals()?== всегда O(1), но equals() для identical объектов тоже O(1) через fast path.
  • Контракт equals()? — Рефлексивность, симметричность, транзитивность, консистентность, x.equals(null) → false.

Красные флаги (НЕ говорить):

  • ❌ “== сравнивает содержимое строк” — сравнивает только ссылки
  • ❌ “equals() всегда медленный” — fast path делает его O(1) для identical объектов
  • ❌ “Можно сравнивать строки через == если они одинаковые” — работают только для литералов в одном классе
  • ❌ “equals() не работает с null” — корректно возвращает false для null аргумента

Связанные темы:

  • [[1. Как работает String Pool]]
  • [[2. В чём разница между созданием String через литерал и через new]]
  • [[9. Можно ли использовать == для сравнения String]]
  • [[4. Почему String является иммутабельным]]