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

Можно ли использовать == для сравнения String?

Технически — да, но в 99% случаев это будет ошибкой.

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

🟢 Junior Level

Технически — да, но в 99% случаев это будет ошибкой.

Оператор == сравнивает ссылки (адреса в памяти), а не содержимое строк.

Пример проблемы:

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

System.out.println(s1 == s2);      // false — разные объекты!
System.out.println(s1.equals(s2)); // true — содержимое одинаковое

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

Когда == сработает:

String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true — оба из String Pool, один объект

Но полагаться на это нельзя — данные из БД или файла будут в обычной куче, не в пуле.


🟡 Middle Level

Когда == вернёт true

Сценарий Код Результат ==
Два литерала "A" == "A" true (String Pool)
Литерал + new "A" == new String("A") false
new + new new String("A") == new String("A") false
Присваивание s1 = s2; s1 == s2 true (одна ссылка)
После intern() s1.intern() == s2.intern() true

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

  1. Ошибка: if (str == "admin") — сработает только если "admin" из пула и str тоже Решение: if ("admin".equals(str)) — безопасно и с константой слева (null-safe)

  2. Ошибка: Сравнение строк из БД через == Решение: Строки из ResultSet — всегда новые объекты. Только equals()

Практическое правило

// ПЛОХО
if (status == "ACTIVE") { ... }

// ХОРОШО
if ("ACTIVE".equals(status)) { ... }

// ЛУЧШЕ (null-safe)
if (Objects.equals(status, "ACTIVE")) { ... }

🔴 Senior Level

Internal Implementation

Оператор ==:

  • JVM-инструкция: if_acmpeq (if address compare equal)
  • Сравнивает значения ссылок (32/64-bit адреса)
  • Время: O(1) — одно сравнение указателей

String.equals():

public boolean equals(Object anObject) {
    if (this == anObject) return true;       // Оптимизация: identity check
    if (anObject instanceof String another) {
        if (coder == another.coder) {         // Same encoding?
            return isLatin1()
                ? StringLatin1.equals(value, another.value)
                : StringUTF16.equals(value, another.value);
        }
    }
    return false;
}
  • Identity check (==) как fast path
  • Затем: проверка coder, длины, посимвольное сравнение
  • Время: O(n) — посимвольный перебор

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

Когда == может быть оправдано:

В ultra-low-latency системах (HFT, trading), где каждая наносекунда важна:

// Все строки предварительно интернированы
String eventType = parseEvent().intern();
if (eventType == ORDER_EVENT) { // O(1) вместо O(n)
    processOrder();
}
  • Профит: 1 CPU цикл vs n циклов (длина строки)
  • Риск: одна неинтернированная строка — и логика сломана

Почему это почти никогда не стоит риска:

  • equals() имеет fast path == — для identical объектов returns мгновенно
  • JVM инлайнит equals() для коротких строк
  • CPU branch prediction хорошо работает с посимвольным сравнением

Edge Cases

  1. Constant folding: "A" + "B" == "AB"true (компилятор схлопывает оба в "AB")

  2. Runtime конкатенация:
    String a = "A";
    String ab = a + "B";
    System.out.println(ab == "AB"); // false — runtime concatenation
    
  3. Interned strings:
    String s1 = new String("test").intern();
    String s2 = "test";
    System.out.println(s1 == s2); // true — оба из пула
    
  4. String deduplication (G1 GC): НЕ делает == true! Дедупликация объединяет byte[], но объекты String остаются разными.

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

| Операция | Время | Когда использовать | | ————————- | ————————– | —————————————- | | == | ~0.3ns (1 CPU cycle) | Только после гарантированного intern() | | equals() (identical) | ~0.3ns (fast path) | Всегда — fast path catches identity | | equals() (same content) | ~2-10ns (зависит от длины) | Всегда — правильный выбор |

equals() для коротких строк оптимизируется JVM через SIMD-инструкции (сравнение 8-16 байт за такт), поэтому разница с == минимальна.

Production Experience

Сценарий: Парсер протокола (10M messages/sec):

  • Разработчик использовал == для сравнения event types после intern()
  • Через месяц: новая версия библиотеки вернула неинтернированные строки
  • Bug: 0.01% сообщений обработаны неверно → financial loss
  • Fix: equals() — overhead 5ns на 10M = 50ms/sec (приемлемо)

Monitoring

// Assert что строки интернированы (в тестах)
assert s1 == s2 : "Strings should be interned";

// JMH для бенчмарка
@Benchmark
public boolean testEquals() { return s1.equals(s2); }

@Benchmark
public boolean testIdentity() { return s1 == s2; }

Best Practices для Highload

  • Дефолт: всегда equals() или Objects.equals()
  • null-check: "CONSTANT".equals(variable) или Objects.equals(a, b)
  • Low-latency edge case: == допустим в двух случаях: (1) проверка на null, (2) гарантированно интернированные строки.
    • Есть unit-тесты, проверяющие эту гарантию
    • Бенчмарк показывает, что разница критична
  • Для сравнения с null: == — это правильный выбор

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

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

  • == сравнивает ссылки (адреса), equals() — содержимое
  • == вернёт true для литералов ("A" == "A"), но false для new String("A")
  • equals() имеет fast path: если == true — сразу returns, не сравнивая посимвольно
  • После intern() обе строки из пула — == вернёт true
  • String deduplication (G1 GC) НЕ делает == true — объекты разные, только byte[] общие
  • В ultra-low-latency системах == допустим для гарантированно интернированных строк

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

  • Когда == вернёт true? — Для литералов, присваиваний (s1 = s2), после intern(), constant folding ("A"+"B" == "AB").
  • Почему equals() имеет fast path ==? — Если это один объект — не нужно сравнивать содержимое. Экономит время для identical references.
  • Можно ли использовать == после intern()? — Да, но это risky: одна неинтернированная строка — и логика сломана. equals() безопаснее.
  • Что будет при runtime конкатенация == литерал?false. String a = "A"; (a + "B") == "AB" — runtime concatenation создаёт новый объект.

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

  • ❌ “== сравнивает содержимое” — сравнивает только ссылки
  • ❌ “equals() всегда медленнее ==” — для identical объектов equals() = == через fast path
  • ❌ “String deduplication делает == true” — нет, объекты остаются разными
  • ❌ “== после конкатенации сработает” — runtime конкатенация создаёт новый объект

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

  • [[10. В чём разница между == и equals() для String]]
  • [[1. Как работает String Pool]]
  • [[3. Когда стоит использовать intern()]]