1. Вступление
В этой статье мы собираемся сравнить две реализации Map :
TreeMap и HashMap .
Обе реализации являются неотъемлемой частью Java Collections Framework и хранят данные в виде пар key-value .
2. Различия
2.1. Реализация
Сначала поговорим о HashMap , который является реализацией на основе хеш-таблицы. Он расширяет класс AbstractMap и реализует интерфейс Map . HashMap работает по принципу hashing .
Эта реализация Map обычно действует как пакетная hash table , но когда корзины становятся слишком большими, они преобразуются в узлы TreeNodes , каждый из которых структурирован аналогично узлам в java.util.TreeMap.
Вы можете найти более подробную информацию о HashMap’s внутренности в ссылке:/java-hashmap[статья посвящена этому].
С другой стороны, TreeMap расширяет класс AbstractMap и реализует интерфейс NavigableMap . TreeMap хранит элементы карты в дереве Red-Black , которое представляет собой Self-Balancing Binary Search Tree .
И вы также можете найти больше информации о внутренностях TreeMap’s по ссылке:/java-treemap[статья сосредоточена на этом здесь].
2.2. Порядок
-
HashMap не предоставляет никаких гарантий относительно расположения элементов в Map ** .
Это означает, что мы не можем принимать любой порядок при переборе keys и values HashMap :
@Test
public void whenInsertObjectsHashMap__thenRandomOrder() {
Map<Integer, String> hashmap = new HashMap<>();
hashmap.put(3, "TreeMap");
hashmap.put(2, "vs");
hashmap.put(1, "HashMap");
assertThat(hashmap.keySet(), containsInAnyOrder(1, 2, 3));
}
Однако элементы в TreeMap сортируются в соответствии с их естественным порядком .
Если объекты TreeMap не могут быть отсортированы в соответствии с естественным порядком, то мы можем использовать Comparator или Comparable для определения порядка, в котором элементы расположены в Map:
@Test
public void whenInsertObjectsTreeMap__thenNaturalOrder() {
Map<Integer, String> treemap = new TreeMap<>();
treemap.put(3, "TreeMap");
treemap.put(2, "vs");
treemap.put(1, "HashMap");
assertThat(treemap.keySet(), contains(1, 2, 3));
}
2.3. Null Значения
HashMap позволяет хранить не более одного null key и многих null значений.
Давайте посмотрим на пример:
@Test
public void whenInsertNullInHashMap__thenInsertsNull() {
Map<Integer, String> hashmap = new HashMap<>();
hashmap.put(null, null);
assertNull(hashmap.get(null));
}
Однако TreeMap не допускает null key , но может содержать много значений null .
Ключ null не разрешен, потому что метод compareTo () или compare () вызывает исключение NullPointerException:
@Test(expected = NullPointerException.class)
public void whenInsertNullInTreeMap__thenException() {
Map<Integer, String> treemap = new TreeMap<>();
treemap.put(null, "NullPointerException");
}
-
Если мы используем TreeMap с пользовательским Comparator , то от того, как обрабатываются null значения, зависит реализация метода _ () _ метода. **
3. Анализ производительности
Производительность является наиболее важным показателем, который помогает нам понять пригодность структуры данных с учетом варианта использования.
В этом разделе мы предоставим всесторонний анализ производительности для HashMap и TreeMap.
3.1. HashMap
-
HashMap, будучи реализацией на основе хеш-таблицы, внутренне использует структуру данных на основе массива для организации своих элементов в соответствии с функцией hash . **
HashMap обеспечивает ожидаемую производительность в постоянное время O (1) для большинства операций, таких как add () , remove () и contains () . Следовательно, это значительно быстрее, чем TreeMap .
Среднее время поиска элемента по разумному предположению в хеш-таблице равно O (1) . Но неправильная реализация функции hash может привести к плохому распределению значений в сегментах, что приводит к:
-
Затраты памяти - многие ведра остаются неиспользованными
-
Снижение производительности - чем больше количество столкновений, тем
снизить производительность
-
До Java 8 Separate Chaining был единственным предпочтительным способом обработки коллизий. ** Обычно он реализован с использованием связанных списков, i.e. , если есть какая-либо коллизия или два разных элемента имеют одинаковое значение хеш-функции, тогда сохраняются оба элемента в тот же связанный список.
Следовательно, поиск элемента в HashMap, в худшем случае мог бы занять столько же времени, сколько и поиск элемента в связанном списке i.e. O (n) .
-
Однако, с появлением JEP 180 , в реализации способа расположения элементов в __ HashMap произошло небольшое изменение
Согласно спецификации, когда сегменты становятся слишком большими и содержат достаточно узлов, они преобразуются в режимы TreeNodes , каждый из которых структурирован аналогично режимам TreeMap .
-
Следовательно, в случае коллизий с большим хешем производительность в худшем случае улучшится с O (n) до O (log n) . **
Код, выполняющий это преобразование, показан ниже:
if(binCount >= TREEIFY__THRESHOLD - 1) {
treeifyBin(tab, hash);
}
Значение TREEIFY THRESHOLD__ равно восьми, что фактически обозначает пороговое значение для использования дерева, а не связанный список для корзины.
Очевидно, что:
-
HashMap требует гораздо больше памяти, чем необходимо для хранения своих данных
-
HashMap не должен быть заполнен более чем на 70% - 75%. Если это близко,
он изменяется и записи перефразируются ** Перефразирование требует n операций, что является дорогостоящим, когда наша постоянная
время вставки становится порядка O (n) ** Это алгоритм хеширования, который определяет порядок вставки
объекты в HashMap
-
Производительность HashMap можно настроить, установив пользовательские initialacity и load factor ** во время самого создания объекта HashMap .
Тем не менее, мы должны выбрать HashMap , если:
-
мы знаем приблизительно, сколько предметов нужно сохранить в нашей коллекции
-
мы не хотим извлекать предметы в естественном порядке
При указанных выше обстоятельствах HashMap является нашим лучшим выбором, поскольку он предлагает постоянное время вставки, поиска и удаления.
3.2. TreeMap
TreeMap сохраняет свои данные в иерархическом дереве с возможностью сортировки элементов с помощью пользовательского Comparator.
Краткое описание его производительности:
-
TreeMap обеспечивает производительность O (log (n)) для большинства операций
как add () , remove () и contains () ** Treemap может сэкономить память (по сравнению с HashMap) , потому что это
использует только объем памяти, необходимый для хранения своих предметов, в отличие от HashMap , который использует непрерывную область памяти ** Дерево должно поддерживать свой баланс, чтобы сохранить его предназначение
производительность, это требует значительных усилий, следовательно, усложняет реализацию
Мы должны пойти на TreeMap всякий раз, когда:
-
ограничения памяти должны быть приняты во внимание
-
мы не знаем, сколько предметов нужно хранить в памяти
-
мы хотим извлечь объекты в естественном порядке
-
если элементы будут последовательно добавляться и удаляться
-
мы готовы принять O (log n) время поиска
4. сходства
4.1. Уникальные элементы
И TreeMap , и HashMap не поддерживают дубликаты ключей. Если он добавлен, он переопределяет предыдущий элемент (без ошибки или исключения):
@Test
public void givenHashMapAndTreeMap__whenputDuplicates__thenOnlyUnique() {
Map<Integer, String> treeMap = new HashMap<>();
treeMap.put(1, "Baeldung");
treeMap.put(1, "Baeldung");
assertTrue(treeMap.size() == 1);
Map<Integer, String> treeMap2 = new TreeMap<>();
treeMap2.put(1, "Baeldung");
treeMap2.put(1, "Baeldung");
assertTrue(treeMap2.size() == 1);
}
4.2. Параллельный доступ
-
Обе реализации Map не являются synchronized ** , и нам нужно управлять параллельным доступом самостоятельно.
Оба должны быть синхронизированы извне всякий раз, когда несколько потоков обращаются к ним одновременно, и по крайней мере один из потоков изменяет их.
Мы должны явно использовать Collections.synchronizedMap (mapName) , чтобы получить синхронизированное представление предоставленной карты.
4.3. Отказоустойчивые итераторы
Iterator генерирует исключение ConcurrentModificationException , если Map изменяется каким-либо образом и в любое время после создания итератора.
Кроме того, мы можем использовать метод удаления итератора для изменения Map во время итерации.
Давайте посмотрим на пример:
@Test
public void whenModifyMapDuringIteration__thenThrowExecption() {
Map<Integer, String> hashmap = new HashMap<>();
hashmap.put(1, "One");
hashmap.put(2, "Two");
Executable executable = () -> hashmap
.forEach((key,value) -> hashmap.remove(1));
assertThrows(ConcurrentModificationException.class, executable);
}
5. Какую реализацию использовать?
В целом, обе реализации имеют свои плюсы и минусы, однако речь идет о понимании основополагающих ожиданий и требований, которые должны определять наш выбор в отношении одного и того же.
Подводя итог:
-
Мы должны использовать TreeMap , если мы хотим, чтобы наши записи были отсортированы
-
Мы должны использовать HashMap , если мы отдаем приоритет производительности над памятью
потребление ** Поскольку TreeMap имеет более существенное местоположение, мы могли бы рассмотреть
это если мы хотим получить доступ к объектам, которые находятся относительно близко друг к другу в соответствии с их естественным порядком ** HashMap может быть настроен с помощью initialCapacity и loadFactor ,
что невозможно для TreeMap ** Мы можем использовать LinkedHashMap , если мы хотим сохранить порядок вставки
пользуясь постоянным доступом
6. Заключение
В этой статье мы показали различия и сходства между TreeMap и HashMap .
Как всегда, примеры кода для этой статьи доступны на GitHub over .