Питання 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-бітні адреси)
  • Час: 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 об’єктів повертає миттєво
  • 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 конкатенація
    
  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 ловить 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 — одразу повертає, не порівнюючи посимвольно
  • Після 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 конкатенація створює новий об’єкт.

Червоні прапорці (НЕ говорити):

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

Пов’язані теми:

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