Перейти к основному содержимому

Руководство по TreeSet в Java

· 8 мин. чтения

1. Обзор

В этой статье мы рассмотрим неотъемлемую часть Java Collections Framework и одну из самых популярных реализаций SetTreeSet .

2. Введение в TreeSet

Проще говоря, TreeSet — это отсортированная коллекция, которая расширяет класс AbstractSet и реализует интерфейс NavigableSet .

Вот краткий обзор наиболее важных аспектов этой реализации:

  • Он хранит уникальные элементы
  • Он не сохраняет порядок вставки элементов
  • Сортирует элементы в порядке возрастания
  • Это не потокобезопасно

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

Проще говоря, будучи самобалансирующимся бинарным деревом поиска, каждый узел бинарного дерева содержит дополнительный бит, который используется для определения цвета узла: красного или черного. Во время последующих вставок и удалений эти «цветные» биты помогают обеспечить более или менее сбалансированное дерево.

Итак, давайте создадим экземпляр TreeSet :

Set<String> treeSet = new TreeSet<>();

2.1. TreeSet с параметром сравнения конструктора

При желании мы можем создать TreeSet с помощью конструктора, который позволяет нам определить порядок сортировки элементов с помощью Comparable или Comparator:

Set<String> treeSet = new TreeSet<>(Comparator.comparing(String::length));

Хотя TreeSet не является потокобезопасным, его можно синхронизировать извне с помощью оболочки Collections.synchronizedSet() :

Set<String> syncTreeSet = Collections.synchronizedSet(treeSet);

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

3. Добавить TreeSet ()

Метод add() , как и ожидалось, можно использовать для добавления элементов в TreeSet . Если элемент был добавлен, метод возвращает true, иначе — false.

В контракте метода указано, что элемент будет добавлен только в том случае, если его еще нет в Set .

Давайте добавим элемент в TreeSet :

@Test
public void whenAddingElement_shouldAddElement() {
Set<String> treeSet = new TreeSet<>();

assertTrue(treeSet.add("String Added"));
}

Метод add чрезвычайно важен, так как детали реализации этого метода иллюстрируют, как TreeSet работает внутри , как он использует метод put TreeMap для хранения элементов: ``

public boolean add(E e) {
return m.put(e, PRESENT) == null;
}

Переменная m ссылается на внутреннюю поддержку TreeMap (обратите внимание, что TreeMap реализует NavigateableMap ):

private transient NavigableMap<E, Object> m;

Таким образом, TreeSet внутренне зависит от поддержки NavigableMap , которая инициализируется экземпляром TreeMap при создании экземпляра TreeSet :

public TreeSet() {
this(new TreeMap<E,Object>());
}

Подробнее об этом можно узнать в этой статье .

4. TreeSet содержит ()

Метод contains() используется для проверки наличия данного элемента в данном TreeSet . Если элемент найден, он возвращает true, в противном случае — false.

Давайте посмотрим, как contains() в действии:

@Test
public void whenCheckingForElement_shouldSearchForElement() {
Set<String> treeSetContains = new TreeSet<>();
treeSetContains.add("String Added");

assertTrue(treeSetContains.contains("String Added"));
}

5. Удаление TreeSet()

Метод remove() используется для удаления указанного элемента из набора, если он присутствует.

Если набор содержит указанный элемент, этот метод возвращает значение true.

Давайте посмотрим на это в действии:

@Test
public void whenRemovingElement_shouldRemoveElement() {
Set<String> removeFromTreeSet = new TreeSet<>();
removeFromTreeSet.add("String Added");

assertTrue(removeFromTreeSet.remove("String Added"));
}

6. Очистить набор деревьев ()

Если мы хотим удалить все элементы из набора, мы можем использовать метод clear() :

@Test
public void whenClearingTreeSet_shouldClearTreeSet() {
Set<String> clearTreeSet = new TreeSet<>();
clearTreeSet.add("String Added");
clearTreeSet.clear();

assertTrue(clearTreeSet.isEmpty());
}

7. Размер набора деревьев ()

Метод size() используется для определения количества элементов, присутствующих в TreeSet . Это один из основных методов в API:

@Test
public void whenCheckingTheSizeOfTreeSet_shouldReturnThesize() {
Set<String> treeSetSize = new TreeSet<>();
treeSetSize.add("String Added");

assertEquals(1, treeSetSize.size());
}

8. TreeSet пуст ()

Метод isEmpty() можно использовать, чтобы выяснить, является ли данный экземпляр TreeSet пустым или нет:

@Test
public void whenCheckingForEmptyTreeSet_shouldCheckForEmpty() {
Set<String> emptyTreeSet = new TreeSet<>();

assertTrue(emptyTreeSet.isEmpty());
}

9. Итератор TreeSet()

Метод iterator() возвращает итератор, выполняющий итерацию в порядке возрастания элементов в наборе. Эти итераторы отказоустойчивы .

Мы можем наблюдать восходящий порядок итераций здесь:

@Test
public void whenIteratingTreeSet_shouldIterateTreeSetInAscendingOrder() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.iterator();
while (itr.hasNext()) {
System.out.println(itr.next());
}
}

Кроме того, TreeSet позволяет нам перебирать набор в порядке убывания.

Давайте посмотрим, что в действии:

@Test
public void whenIteratingTreeSet_shouldIterateTreeSetInDescendingOrder() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.descendingIterator();
while (itr.hasNext()) {
System.out.println(itr.next());
}
}

Итератор генерирует исключение ConcurrentModificationException , если набор изменяется в любое время после создания итератора любым способом, кроме как с помощью метода итератора remove() .

Создадим для этого тест:

@Test(expected = ConcurrentModificationException.class)
public void whenModifyingTreeSetWhileIterating_shouldThrowException() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.iterator();
while (itr.hasNext()) {
itr.next();
treeSet.remove("Second");
}
}

В качестве альтернативы, если бы мы использовали метод удаления итератора, мы бы не столкнулись с исключением:

@Test
public void whenRemovingElementUsingIterator_shouldRemoveElement() {

Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Second");
treeSet.add("Third");
Iterator<String> itr = treeSet.iterator();
while (itr.hasNext()) {
String element = itr.next();
if (element.equals("Second"))
itr.remove();
}

assertEquals(2, treeSet.size());
}

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

Подробнее об этом можно узнать здесь .

10. Сначала TreeSet()

Этот метод возвращает первый элемент из TreeSet , если он не пуст. В противном случае выдается исключение NoSuchElementException .

Давайте посмотрим пример:

@Test
public void whenCheckingFirstElement_shouldReturnFirstElement() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");

assertEquals("First", treeSet.first());
}

11. Последний набор деревьев ()

Аналогично приведенному выше примеру, этот метод вернет последний элемент, если набор не пуст:

@Test
public void whenCheckingLastElement_shouldReturnLastElement() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Last");

assertEquals("Last", treeSet.last());
}

12. Подмножество TreeSet()

Этот метод вернет элементы в диапазоне от fromElement до toElement. Обратите внимание, что fromElement является инклюзивным, а toElement — исключающим:

@Test
public void whenUsingSubSet_shouldReturnSubSetElements() {
SortedSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
treeSet.add(5);
treeSet.add(6);

Set<Integer> expectedSet = new TreeSet<>();
expectedSet.add(2);
expectedSet.add(3);
expectedSet.add(4);
expectedSet.add(5);

Set<Integer> subSet = treeSet.subSet(2, 6);

assertEquals(expectedSet, subSet);
}

13. HeadSet TreeSet ()

Этот метод вернет элементы TreeSet , которые меньше указанного элемента:

@Test
public void whenUsingHeadSet_shouldReturnHeadSetElements() {
SortedSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
treeSet.add(5);
treeSet.add(6);

Set<Integer> subSet = treeSet.headSet(6);

assertEquals(subSet, treeSet.subSet(1, 6));
}

14. Набор деревьев tailSet()

Этот метод вернет элементы TreeSet , которые больше или равны указанному элементу:

@Test
public void whenUsingTailSet_shouldReturnTailSetElements() {
NavigableSet<Integer> treeSet = new TreeSet<>();
treeSet.add(1);
treeSet.add(2);
treeSet.add(3);
treeSet.add(4);
treeSet.add(5);
treeSet.add(6);

Set<Integer> subSet = treeSet.tailSet(3);

assertEquals(subSet, treeSet.subSet(3, true, 6, true));
}

15. Хранение нулевых элементов

До Java 7 можно было добавлять пустые элементы в пустой TreeSet .

Однако это посчитали ошибкой. Поэтому TreeSet больше не поддерживает добавление null.

Когда мы добавляем элементы в TreeSet, элементы сортируются в соответствии с их естественным порядком или в порядке, указанном компаратором. Следовательно, добавление нулевого значения по сравнению с существующими элементами приводит к исключению NullPointerException , поскольку нуль нельзя сравнивать ни с каким значением:

@Test(expected = NullPointerException.class)
public void whenAddingNullToNonEmptyTreeSet_shouldThrowException() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add(null);
}

Элементы, вставленные в TreeSet, должны либо реализовывать интерфейс Comparable , либо , по крайней мере, быть приняты указанным компаратором. Все такие элементы должны быть взаимно сравнимы, т . е . e1.compareTo(e2) или comparator.compare(e1, e2) не должны вызывать исключение ClassCastException .

Давайте посмотрим пример:

class Element {
private Integer id;

// Other methods...
}

Comparator<Element> comparator = (ele1, ele2) -> {
return ele1.getId().compareTo(ele2.getId());
};

@Test
public void whenUsingComparator_shouldSortAndInsertElements() {
Set<Element> treeSet = new TreeSet<>(comparator);
Element ele1 = new Element();
ele1.setId(100);
Element ele2 = new Element();
ele2.setId(200);

treeSet.add(ele1);
treeSet.add(ele2);

System.out.println(treeSet);
}

16. Производительность TreeSet

По сравнению с HashSet производительность TreeSet ниже. Такие операции, как добавление , удаление и поиск , занимают время O(log n) , в то время как такие операции, как печать n элементов в отсортированном порядке, требуют времени O(n) .

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

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

Когда мы говорим о местности:

  • Приложение часто обращается к аналогичным данным с одинаковой частотой.
  • Если две записи находятся рядом с заданным порядком, TreeSet помещает их рядом друг с другом в структуре данных и, следовательно, в памяти.

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

В случае, если данные должны быть прочитаны с жесткого диска (который имеет большую задержку, чем данные, прочитанные из кеша или памяти), тогда предпочтите TreeSet , поскольку он имеет большую локальность.

17. Заключение

В этой статье мы сосредоточимся на понимании того, как использовать стандартную реализацию TreeSet в Java. Мы увидели его цель и то, насколько он эффективен в отношении удобства использования, учитывая его способность избегать дубликатов и сортировать элементы.

Как всегда, фрагменты кода можно найти на GitHub .