Однажды в университете мне нужно было написать задачу для сортировки в порядке возрастания фамилий, которые были ключами к личным делам моих одногруппников, и я потратил на это немало времени. Но если бы тогда я был знаком с классом TreeMap, это у меня получилось бы гораздо быстрее.

Что такое TreeMap? Это структура данных, похожая на словарь, которая позволяет сохранять элементы в виде ключ-значение, используя сортировку по ключам.

Где и как его можно использовать? Например, в том же случае с одногруппниками. Если мне нужно хранить значения в порядке возрастания, вместо того, чтобы писать свои алгоритмы сортировки, достаточно создать TreeMap и положить значения туда.

Для таких типов как Integer и String используется порядок сортировки по возрастанию. Но если ты хочешь положить в TreeMap собственный тип, нужно, чтобы твой класс имплементировал интерфейс Comparable, в классе был реализован метод compareTo() и указано, как нужно сортировать значения данного класса.


public class Person implements Comparable<Person> {
 
    private String firstName;
    private String lastName;
 
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
  ….
 
    @Override
    public int compareTo(Person person) {
        return person.getFirstName().compareTo(firstName);
    }
 
    @Override
    public String toString() {
        return "Person{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

Переопределим метод compareTo(), чтобы он сортировал значение в порядке алфавитного убывания имени:


TreeMap map = new TreeMap<Person, String>();
 
map.put(new Person("AA","BB"), "aa");
map.put(new Person("BB","BB"), "aa");
map.put(new Person("DD","BB"), "aa");
map.put(new Person("CC","BB"), "aa");

Значения будут храниться в следующем порядке:


Person{firstName='DD', lastName='BB'}
Person{firstName='CC', lastName='BB'}
Person{firstName='BB', lastName='BB'}
Person{firstName='AA', lastName='BB'}

Класс TreeMap реализует интерфейс NavigableMap, который в свою очередь расширяет SortedMap. Это позволяет классу TreeMap использовать для хранения деревья и хранить значения в отсортированном порядке.

Дерево, как говорит Википедия, — это самобалансирующая двоичная структура поиска, для хранения данных, которая использует узлы для хранения, сравнивая перед этим значения.

Если говорить простыми словами, красно-чёрное дерево — это структура данных, хранящая справа значения, которые больше корневого, а слева — те, что меньше. Такая реализация помогает быстрее искать значения в структуре.

Красно-черное дерево самобалансирующееся, поэтому при каждой новой вставке оно будет менять свою структуру, так как добавляются новые значения. Корневым считается значение, добавленное первым, но в процессе балансировки, корневым может стать и другое значение.

Итак, теперь ты знаешь, что такое TreeMap и по какому принципу оно работает.

Важно помнить то, что хранить в TreeMap можно только тех, кто имплементировал Comparable интерфейс и переопределил метод compareTo().

Но что делать, если мы используем сторонние классы, которые подгружаем из других библиотек, и не можем имплементировать в них Comparable? Для таких случаев есть решение: написать свой Comparator.

Comparator — это интерфейс, в котором есть метод compare(). С его помощью мы можем сравнивать объекты и хранить их в TreeMap.


Comparator<Person> comparator = new Comparator<Person>() {
 
    @Override
    public int compare(Person person1, Person person2) {
        return person1.getFirstName().compareTo(person2.getFirstName());
    }
};
 
 
TreeMap map = new TreeMap<Person, String>(comparator);

В этом примере мы создали кастомный Comparator и передали TreeMap классу.

Начиная с Java 8 это можно написать, используя лямбда-выражение:


TreeMap map = new TreeMap<Person, String>((Person person1, Person person2) -> person1.getFirstName().compareTo(person2.getFirstName()) );

То есть, чтобы хранить значения в TreeMap, нужно указать, как их сортировать. Сделать это можно двумя способами: имплементировать Comparable или реализовать свой Comparator.

Но что если нам нужно положить в TreeMap значение null как ключ? HashMap позволяет это сделать. А как же работает с этим TreeMap?


TreeMap map = new TreeMap<Person, String>();
map.put (null, "Person");

Запустив этот код, получим ошибку:

Exception in thread "main" java.lang.NullPointerException at java.base/java.util.TreeMap.put(TreeMap.java:561)

Проблема заключается в том, что внутри класса TreeMap происходит сравнение значений методом compareTo(). Ты, конечно же, можешь положить туда null значение, и оно скомпилируется, но в рантайме ты получишь ошибку, так как на значении null вызовется метод и вылетит NullPointerException.

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

В отличие от TreeMap, в HashMap разрешено хранить null как ключ. Для всех null ключей выделено определенное место в структуре. Хранить null ключи в HashMap позволяет то, что определение места здесь происходит по значению хэша, и для этого не нужно сравнивать значения с другими. Поэтому у всех null ключи есть свое место.

Итак, теперь ты знаешь, что использовать, когда тебе нужно хранить значения в отсортированном порядке, и как задавать правила и порядок сортировки.