JavaRush /Java блог /Java Developer /Особенности TreeMap в Java
Автор
Василий Малик
Senior Java-разработчик в CodeGym

Особенности TreeMap в Java

Статья из группы Java Developer
Если ты читаешь эту статью, скорее всего, ты знаком с интерфейсом Map и вариантами его применения. Если нет, то тебе сюда. Сегодня мы поговорим об особенностях реализации TreeMap, а конкретнее — чем она отличается от HashMap и как правильно ее использовать.

Сравнение TreeMap, HashMap и LinkedHashMap

Наиболее используемая имплементация интерфейса Map — это HashMap. Она простая в использовании и гарантирует быстрый доступ к данным, поэтому это лучший кандидат для решения большинства задач. Большинства, но не всех. Иногда необходимо хранить данные в структурированном виде с возможностью навигации по ним. В таком случае на помощь приходит другая реализация интерфейса MapTreeMap. TreeMap имплементирует интерфейс NavigableMap, который наследуется от SortedMap, а он, в свою очередь от интерфейса Map. Особенности TreeMap - 2Имплементируя интерфейсы NavigableMap и SortedMap, TreeMap получает дополнительный функционал, которого нет в HashMap, но плата за это — производительность. Существует еще класс LinkedHashMap, который тоже позволяет хранить данные в определенном порядке — в порядке добавления. Чтобы тебе были понятны различия между этими тремя классами, посмотри на эту таблицу:
HashMap LinkedHashMap TreeMap
Порядок хранения данных Случайный. Нет гарантий, что порядок сохранится на протяжении времени В порядке добавления В порядке возрастания или исходя из заданного компаратора
Время доступа к элементам O(1) O(1) O(log(n))
Имплементированные интерфейсы Map Map NavigableMap
SortedMap
Map
Имплементация на основе структуры данных Корзины (buckets) Корзины (buckets) Красно-чёрное дерево (Red-Black Tree)
Возможность работы с null-ключом Можно Можно Можно, если используется компаратор, разрешающий null
Потокобезопасна Нет Нет Нет
Как видишь, в этих классах есть много общего, но и есть несколько отличий. Хоть класс TreeMap является самым многофункциональным, он не всегда может хранить null в качестве ключа. Кроме этого, время доступа к элементам TreeMap будем самым длительным. Поэтому если тебе не нужно хранить данные в отсортированном виде, лучше используй HashMap или LinkedHashMap.

Красно-чёрное дерево

Как ты наверняка заметил, под капотом TreeMap использует структуру данных, которая называется красно-чёрное дерево. Именно хранение данных в этой структуре и обеспечивает порядок хранения данных. Что же представляет собой это дерево? Давай разбираться! Представь, что тебе необходимо хранить пары “Число-Строка”. Числа 16, 20, 52, 55, 61, 65, 71, 76, 81, 85, 90, 93, 101 будут ключами. Если ты хранишь данные в традиционном списке и появляется необходимость найти элемент с ключом 101, нужно будет перебрать все 13 элементов в его поисках. Для 13 элементов это не критично, при работе с миллионом у нас возникнут большие неприятности. Для решения таких проблем программисты используют немного более сложные структуры данных. Поэтому встречай красно-чёрное дерево! Особенности TreeMap - 3

https://algorithmtutor.com/Data-Structures/Tree/Red-Black-Trees/

Поиск нужного элемента начинается из корня дерева, в нашем случае это 61. Дальше происходит сравнение с необходимым значением. Если наше значение меньше — отправляемся в левую сторону, если больше — в правую. Так происходит до тех пор, пока не найдем необходимое значение или не упремся в элемент со значением null (листок дерева). Красные и черные цвета используются для упрощения навигации по дереву и его балансировки. Существуют правила, которые всегда должны быть соблюдены при постройке красно-черного дерева:
  • Корень должен быть окрашен в черный цвет.
  • Листья дерева должны быть черного цвета.
  • Красный узел должен иметь два черных дочерних узла.
  • Черный узел может иметь любые дочерние узлы.
  • Путь от узла к его листьям должен содержать одинаковое количество черных узлов.
  • Новые узлы добавляются на места листьев.
Если посмотреть на правила 3, 4 и 5 в совокупности, можно понять, как окраска узлов ускоряет навигацию по дереву: путь через черные узлы всегда короче, чем через красные. Поэтому по количеству именно черных узлов и определяется общий размер дерева, и называется этот размер “черная высота”. Красно-черное дерево реализуют на разных языках программирования. В интернете существует куча реализаций для Java, поэтому не будем на нем останавливаться надолго, а продолжим знакомство с функционалом TreeMap.

Методы, полученные из интерфейсов SortedMap и NavigableMap

Как и HashMap, TreeMap имплементирует интерфейс Map, а это значит, что в TreeMap есть все те методы, что и в HashMap. Но вдобавок TreeMap реализует интерфейсы SortedMap и NavigableMap, получая дополнительный функционал из них. SortedMap — интерфейс, который расширяет Map и добавляет методы, актуальные для отсортированного набора данных:
  • firstKey(): возвращает ключ первого элемента мапы;
  • lastKey(): возвращает ключ последнего элемента;
  • headMap(K end): возвращает мапу, которая содержит все элементы текущей, от начала до элемента с ключом end;
  • tailMap(K start): возвращает мапу, которая содержит все элементы текущей, начиная с элемента start и до конца;
  • subMap(K start, K end): возвращает мапу, которая содержит все элементы текущей,начиная с элемента start и до элемента с ключом end.
NavigableMap — интерфейс, который расширяет SortedMap и добавляет методы для навигации между элементами мапы:
  • firstEntry(): возвращает первый пару “ключ-значение”;
  • lastEntry(): возвращает последнюю пару “ключ-значение”;
  • pollFirstEntry(): возвращает и удаляет первую пару;
  • pollLastEntry(): возвращает и удаляет последнюю пару;
  • ceilingKey(K obj): возвращает наименьший ключ k, который больше или равен ключу obj. Если такого ключа нет, возвращает null;
  • floorKey(K obj): возвращает самый большой ключ k, который меньше или равен ключу obj. Если такого ключа нет, возвращает null;
  • lowerKey(K obj): возвращает наибольший ключ k, который меньше ключа obj. Если такого ключа нет, возвращает null;
  • higherKey(K obj): возвращает наименьший ключ k, который больше ключа obj. Если такого ключа нет, возвращает null;
  • ceilingEntry(K obj): аналогичен методу ceilingKey(K obj), только возвращает пару “ключ-значение” (или null);
  • floorEntry(K obj): аналогичен методу floorKey(K obj), только возвращает пару “ключ-значение” (или null);
  • lowerEntry(K obj): аналогичен методу lowerKey(K obj), только возвращает пару “ключ-значение” (или null);
  • higherEntry(K obj): аналогичен методу higherKey(K obj), только возвращает пару “ключ-значение” (или null);
  • descendingKeySet(): возвращает NavigableSet, содержащий все ключи, отсортированные в обратном порядке;
  • descendingMap(): возвращает NavigableMap, содержащую все пары, отсортированные в обратном порядке;
  • navigableKeySet(): возвращает объект NavigableSet, содержащий все ключи в порядке хранения;
  • headMap(K upperBound, boolean incl): возвращает мапу, которая содержит пары от начала и до элемента upperBound. Аргумент incl указывает, нужно ли включать элемент upperBound в возвращаемую мапу;
  • tailMap(K lowerBound, boolean incl): функционал похож на предыдущий метод, только возвращаются пары от lowerBound и до конца;
  • subMap(K lowerBound, boolean lowIncl, K upperBound, boolean highIncl): как и в предыдущих методах, возвращаются пары от lowerBound и до upperBound, аргументы lowIncl и highIncl указывают, включать ли граничные элементы в новую мапу.
В самой же реализации TreeMap, кроме привычных нам конструкторов, добавляется еще один, который принимает экземпляр компаратора. Этот компаратор и будет отвечать за порядок хранения элементов.

Примеры использования TreeMap

Такое изобилие дополнительных методов может показаться ненужным, но очень часто они оказываются куда полезнее, чем казалось изначально. Давай рассмотрим с тобой вот такой пример. Представь, что мы работаем в маркетинговом отделе большой компании, и у нас есть база людей, которым мы хотим показывать рекламу. При этом есть два нюанса:
  • нам нужно вести учет количества показов каждому человеку;
  • алгоритм показа рекламы для несовершеннолетних отличается.
Создадим класс Person, в котором будет храниться вся доступная нам информация о человеке:

public class Person {
   public String firstName;
   public String lastName;
   public int age;

   public Person(String firstName, String lastName, int age) {
       this.firstName = firstName;
       this.lastName = lastName;
       this.age = age;
   }
}
Логику реализуем в классе Main:

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

public class Main {
   public static void main(String[] args) {
      TreeMap<Person, Integer> map = new TreeMap<>(Comparator.comparingInt(o -> o.age));
      map.put(new Person("John", "Smith", 17), 0);
      map.put(new Person("Ivan", "Petrenko", 65), 0);
      map.put(new Person("Pedro", "Escobar", 32), 0);
      map.put(new Person("Radion", "Pyatkin", 14), 0);
      map.put(new Person("Sergey", "Vashkevich", 19), 0);

      Person firstAdultPerson = map.navigableKeySet().stream().filter(person -> person.age>18).findFirst().get();

       Map<Person, Integer> youngPeopleMap = map.headMap(firstAdultPerson, false);
       Map<Person, Integer> adultPeopleMap = map.tailMap(firstAdultPerson, true);
       showAdvertisementToYoung(youngPeopleMap);
       showAdvertisementToAdult(adultPeopleMap);
   }

   public static void showAdvertisementToYoung(Map map){}
   public static void showAdvertisementToAdult(Map map){}
}
В классе Main создаем TreeMap, где key — это конкретный человек, а value — количество показов рекламы в этом месяце. В конструкторе передаем компаратор, который отсортирует людей по возрасту. Заполняем map рандомными значениями. Теперь нам нужно получить ссылку на первого взрослого человека в нашем мини-хранилище данных. Делаем это с помощью Stream API. После этого получаем две независимые мапы, которые передаем в методы, показывающие рекламу. Существует очень много способов, которыми можно было бы решить эту задачу. Арсенал методов класса TreeMap позволяет изобретать решения на любой вкус. Запоминать их все не обязательно, ведь всегда можно воспользоваться документацией или подсказками среды разработки. На этом все! Надеюсь, теперь класс TreeMap для тебя понятен, и ты найдешь ему точное применение в решении практических задач.
Комментарии (32)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #2372013 Уровень 26
2 декабря 2021
Для лучшего понимания принципов работы класса "TreeMap" рекомендую почитать и посмотреть про "красно-черные деревья" , предварительно погуглив про "бинарные" и "2-3-4 деревья" 1 https://www.youtube.com/watch?v=a9EwBVLQ364 2 https://www.youtube.com/watch?v=n7Y2karbxF4&t=6s 3 https://www.happycoders.eu/algorithms/red-black-tree-java/ 4 https://www.youtube.com/watch?v=u-ilAwbJWYc // детальное объяснение класса TreeMap
Dmitry Koichev Уровень 20
30 марта 2021
По какому принципу определяется корень дерева ? это первый добавленный в мэпу элемент или нет ?
Сергей Уровень 38
21 января 2021
При попытке положить в TreeMap null-ключ в любом случае возникает NPE. В лекции написано "Можно, если не используется компаратор" - какая разница, тогда класс ключей должен реализовывать интерфейс Comparable<>.
Deniska Уровень 26
23 декабря 2020
а как получить минимальное значение в treeMap не зная его?
Regina C Уровень 36
12 декабря 2020
все таки в map.put(new Person("Radion", "Pyatkin", 14), 0); - имя Родион, а не Радион)))
Regina C Уровень 36
12 декабря 2020
кто здесь, потому что на 19 уровне есть задача с ключами и зарплатой?
wan-derer.ru Уровень 40
13 ноября 2020
Интересно, но хотелось бы больше примеров этих методов, желательно пока без stream. А то хочу пройти по TreeMap в обратном порядке, а мне IDE красит методы красным и говорит что не понимает чего я хочу.
Arjun Уровень 37
14 октября 2020
Приведенная картинка красно-черного дерева не соответствует описанным правилам. Вообще логика определения красных и черных узлов не понятна.
16 июня 2020
Поэтому если тебе не нужно хранить данные в отсортированном виде, лучше используй HashMap или LinkedHashMap. По умолчанию данные в HashMap хранятся далеко не в отсортированном виде. Хотелось бы больше информации по этому поводу, подскажите где почитать.
Leonid Уровень 41 Expert
13 июня 2020
В таблице для TreeMap: Возможность работы с null-ключем - Можно, если не используется компаратор Это ошибка, опечатка или что? Null ключ можно добавить только если использовать свой компаратор, который работает с null. Например:

Map<Integer, String> map = new TreeMap<>(Comparator.nullsFirst(Comparator.naturalOrder()));