В чому різниця між == та equals() для String?
Алгоритм String.equals():
🟢 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():
- Спочатку перевіряє
==(fast path — якщо це один об’єкт, одразуtrue) - Перевіряє, що об’єкт — це
String - Порівнює поле
coder(однакове чи кодування)
coder(Java 9+) — прапорець, що вказує, чи зберігається рядок у Latin1 (1 байт/символ) чи UTF-16 (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);
}
Типові помилки
-
Помилка:
s1.equals(s2)колиs1може бутиnullРішення:"constant".equals(variable)абоObjects.equals(s1, s2) -
Помилка: Думати, що
==працює для однакових літералів завжди Рішення: Це працює лише для літералів в одному класі. Дані ззовні — у купі.
Порівняння
| Критерій | == | 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
-
Контракт з hashCode(): Якщо
s1.equals(s2)→s1.hashCode() == s2.hashCode(). УStringце дотримується — хеш обчислюється з вмісту. -
Coder mismatch:
Stringз Latin1 ("abc") іStringз UTF-16 ("abc"+ Cyrillic) —equals()повернеfalseна етапі перевіркиcoder, навіть якщо візуально символи однакові. -
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)
==— лише дляnullchecks і гарантовано інтернованих рядків
🎯 Шпаргалка для співбесіди
Обов’язково знати:
==порівнює посилання (адреси у пам’яті),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 є незмінним]]