Питання 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 є незмінним]]