Почему 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()]]