Если я больше 3 месяцев не пишу посты про хэшмэп, значит у меня угнали канал.
Люблю разбирать классы в JDK. Они используются каждый день и интересны сами по себе. А ещё на их примере удобно объяснять разные концепции, хорошие и плохие практики.
На этой неделе расскажу пару неочевидных моментов на основе класса LinkedHashMap.
Начнём с базы. LinkedHashMap - это HashMap, внутри которого есть связный список. По умолчанию список сохраняет порядок вставки элементов:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);
map.put(2, 2);
int lastKey = map.sequencedKeySet().getLast(); // 2
Если в конструкторе передать accessOrder=true, список запоминает порядок извлечения элементов. Вызов get(1) отправляет 1 в конец списка:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(16, 0.8f, true);
map.put(1, 1);
map.put(2, 2);
map.get(1);
int lastKey = map.sequencedKeySet().getLast(); // 1
С теорией закончили, переходим к интересному.
В первую очередь, бросается в глаза флажок в конструкторе. Гораздо симпатичнее в параметрах выглядел бы enum. Что-то вроде ELEMENT_ORDER.INSERT. Подробно бэд пректис с флажками и альтернативы разбирала тут.
Второй момент касается проектирования. Зачем вообще нужен LinkedHashMap с accessOrder=true?
Документация пишет, что это отличная база для LRU кэша. С первого взгляда похоже на правду, но есть пара нюансов:
▫️ У LinkedHashMap нет ограничений на размер. У кэша — есть
▫️ С кэшами работают много потоков. LinkedHashMap - не потокобезопасен, единственный вариант для корректной работы — synchronized обёртка:
Map map = Collections.synchronizedMap(new LinkedHashMap(…))
Получится кэш с пропускной способностью в один поток😐
В итоге:
❌ В чистом виде LinkedHashMap с accessOrder=true нужен либо никому, либо в редких случаях
❌ В качестве LRU кэша (как предлагается в документации) класс использовать сразу не получится. Либо доделывать, либо взять уже готовые и более эффективные реализации кэша.
Подобные опции в API - лишние.
Хорошие библиотеки и фреймворки состоят из двух частей: базовые многофункциональные элементы + удобные методы для популярных кейсов. Так достигается баланс между простотой и гибкостью.
✨ Хороший пример: экзекьюторы ✨
Базовый элемент для экзекьютора — класс ThreadPoolExecutor. В конструкторе 5 параметров, можно переопределить методы. Есть готовые варианты, которые подойдут для большинства задач:
Executors.newFixedThreadPool(int nThreads)
Executors.newSingleThreadExecutor()
Executors.newCachedThreadPool()
Всё вместе - приятное и удобное апи.
Примеры выше простые, но сам подход универсален. Комбо базовых и удобных методов отлично подходит для библиотек интеграции или для общих модулей.
Ответ на вопрос перед постом: с флажком true запоминается порядок извлечения элементов. Подписчики канала - умнички независимо от каких-то флажков🥰