Чому String часто використовують як ключ в HashMap?
Клас String — final та іммутабельний. Це гарантує:
🟢 Junior Level
String — найпопулярніший тип ключа в HashMap. І на те є вагомі причини:
- Незмінність — рядок не можна змінити після створення
- Кешує hashCode — обчислює один раз, потім повертає готове число
- Добре розподіляє — різні рядки дають різні хеші
Приклад:
Map<String, User> usersByName = new HashMap<>();
usersByName.put("alice", new User("Alice"));
usersByName.put("bob", new User("Bob"));
// Швидко, надійно, просто
Аналогія: String — це як паспорт з довічним терміном дії. Він ніколи не змінюється, і за ним завжди можна знайти людину.
🟡 Middle Level
1. Іммутабельність
Клас String — final та іммутабельний. Це гарантує:
hashCode()ніколи не змінитьсяequals()завжди дає передбачуваний результат- Немає ризику “зламаного ключа”
2. Кешування hashCode
public final class String {
private int hash; // Кешується при першому виклику hashCode()
@Override
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = ...; // Обчислення один раз
}
return h;
}
}
При частих get/put кешування hashCode економить повторне обчислення: для рядка довжиною N це O(N) на перший виклик і O(1) на всі наступні.
3. Реалізація Comparable
String реалізує Comparable<String>, що прискорює treeification в Java 8+ при колізіях.
4. String Pool
Однакові рядкові літерали посилаються на один об’єкт:
String a = "userId";
String b = "userId";
System.out.println(a == b); // true — один об'єкт в пам'яті!
Це економить пам’ять і прискорює порівняння через ==.
Типові помилки
new String("key")— створює новий об’єкт, оминаючи String Pool- Рядки із зовнішнього вводу — можуть бути джерелом Hash Flooding
- Дуже довгі рядки — обчислення hashCode дороге (хоч і кешується)
Коли НЕ використовувати String як ключ
- Чутливі дані (паролі, токени) — рядки не очистити з пам’яті, використовуйте char[]
- Дуже довгі рядки — hashCode дорогий (O(N) на кожну операцію)
- International strings без нормалізації — ‘é’ і ‘e\u0301’ будуть різними ключами
🔴 Senior Level
Internal: hashCode алгоритм
// Формула: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
// Множник 31: JVM оптимізує 31*i = (i << 5) - i
Це дає хороший розподіл для більшості текстових даних.
Compact Strings (Java 9+)
В Java 9+ String використовує byte[] замість char[]:
- ASCII/Latin-1: 1 байт на символ (економія 50% пам’яті)
- UTF-16: 2 байти (для не-ASCII)
- Вплив на HashMap: менше пам’яті на ключі → менше GC pressure
Performance Benchmarks
| Операція | String | Custom Object |
|---|---|---|
| hashCode (1-й виклик) | O(n) | O(1) |
| hashCode (повторний) | O(1) (кеш) | O(1) |
| equals | O(n) | O(1) |
| Memory | 24-48 байт | Залежить від полів |
Для коротких рядків (ID, імена полів) String — оптимальний вибір. Для дуже довгих рядків (кілобайти тексту) розгляньте попереднє хешування.
Security: String як ключ
- Hash Flooding: Зловмисник може підібрати рядки з колізіями. Java 8+ захищає через treeification
- Memory: Рядки в String Pool не збираються GC до завершення класу. Великі ключі = витік
- Sensitive data: Рядки з паролями/токенами залишаються в пам’яті. Використовуйте
char[]для секретів
Edge Cases
- Empty string
"": Валідний ключ, hashCode = 0 - International strings: Unicode-нормалізація може впливати на equals (“é” vs “e\u0301”)
- Interned strings:
String.intern()поміщає в пул — економить пам’ять, але Metaspace (раніше PermGen, видалений в Java 8) обмежений. Надмірний intern() може призвести до OutOfMemoryError: Metaspace.
Production Best Practices
- Короткі рядки (ID, імена полів) — ідеальний варіант
- Уникайте довгих рядків як ключів (тексти, JSON) — hashCode дорогий
- Використовуйте
intern()для повторюваних ключів у великих мапах - Валідуйте вхідні рядки — захист від Hash Flooding
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- String immutable (final class) — hashCode ніколи не зміниться
- String кешує hashCode — перший виклик O(n), повторні O(1)
- String Pool: однакові літерали = один об’єкт в пам’яті
- Реалізує Comparable — прискорює treeification при колізіях
- Compact Strings (Java 9+): byte[] замість char[] — економія 50% пам’яті для ASCII
- hashCode алгоритм: s[0]*31^(n-1) + … — множник 31 = (i«5)-i
Часті уточнюючі запитання:
- Чому new String(“key”) гірше ніж “key”? — new String створює окремий об’єкт, оминаючи String Pool
- Коли String поганий ключ? — чутливі дані (паролі), дуже довгі рядки, international strings без нормалізації
- Чому String.intern() небезпечний? — Metaspace обмежений, надмірний intern() → OOM: Metaspace
- Що таке Hash Flooding через рядки? — зловмисник підбирає рядки з колізіями; Java 8+ захищає treeification
Червоні прапорці (НЕ говорити):
- «String.hashCode() = адреса рядка в пам’яті» — ні, формула на основі символів
- «String завжди швидкий ключ» — ні, для дуже довгих рядків hashCode дорогий O(n)
- «String Pool нескінченний» — ні, обмежений Metaspace
Пов’язані теми:
- [[14. Які вимоги пред’являються до ключа HashMap]]
- [[16. Що таке load factor в HashMap]]
- [[7. Який контракт у equals() та hashCode()]]