«1. Обзор
В этой статье мы рассмотрим неотъемлемую часть Java Collections Framework и одну из самых популярных реализаций Set — TreeSet.
2. Введение в TreeSet
Проще говоря, TreeSet — это отсортированная коллекция, которая расширяет класс AbstractSet и реализует интерфейс NavigableSet.
Вот краткий обзор наиболее важных аспектов этой реализации:
-
Хранит уникальные элементы Не сохраняет порядок вставки элементов Сортирует элементы в возрастающем порядке Не является потокобезопасным
В В этой реализации объекты сортируются и сохраняются в порядке возрастания в соответствии с их естественным порядком. TreeSet использует самобалансирующееся бинарное дерево поиска, точнее красно-черное дерево.
Проще говоря, будучи самобалансирующимся бинарным деревом поиска, каждый узел бинарного дерева содержит дополнительный бит, который используется для определения цвета узла: красного или черного. Во время последующих вставок и удалений эти биты «цвета» помогают гарантировать, что дерево останется более или менее сбалансированным.
Итак, давайте создадим экземпляр TreeSet:
Set<String> treeSet = new TreeSet<>();
2.1. TreeSet с Constructor Comparator Param
При желании мы можем создать TreeSet с помощью конструктора, который позволяет нам определить порядок сортировки элементов с помощью Comparable или Comparator:
Set<String> treeSet = new TreeSet<>(Comparator.comparing(String::length));
Хотя TreeSet не является потоком -safe, его можно синхронизировать извне с помощью оболочки Collections.synchronizedSet():
Set<String> syncTreeSet = Collections.synchronizedSet(treeSet);
Хорошо, теперь, когда у нас есть четкое представление о том, как создать экземпляр TreeSet, давайте посмотрим на общие операции, которые у нас есть. доступный.
3. TreeSet add()
Метод add(), как и ожидалось, можно использовать для добавления элементов в TreeSet. Если элемент был добавлен, метод возвращает true, иначе — false.
В контракте метода указано, что элемент будет добавлен только в том случае, если его еще нет в наборе.
Давайте добавим элемент в 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()
Метод 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()
Метод remove() используется для удаления указанного элемента из набора, если он присутствует.
Если набор содержит указанный элемент, этот метод возвращает значение true.
Давайте посмотрим на это в действии:
@Test
public void whenRemovingElement_shouldRemoveElement() {
Set<String> removeFromTreeSet = new TreeSet<>();
removeFromTreeSet.add("String Added");
assertTrue(removeFromTreeSet.remove("String Added"));
}
6. TreeSet clear()
Если мы хотим удалить все элементы из набора, мы можем использовать метод clear():
@Test
public void whenClearingTreeSet_shouldClearTreeSet() {
Set<String> clearTreeSet = new TreeSet<>();
clearTreeSet.add("String Added");
clearTreeSet.clear();
assertTrue(clearTreeSet.isEmpty());
}
~~ ~ 7. TreeSet size()
Метод size() используется для определения количества элементов, присутствующих в TreeSet. Это один из основных методов API:
@Test
public void whenCheckingTheSizeOfTreeSet_shouldReturnThesize() {
Set<String> treeSetSize = new TreeSet<>();
treeSetSize.add("String Added");
assertEquals(1, treeSetSize.size());
}
8. TreeSet isEmpty()
Метод isEmpty() можно использовать для определения того, является ли данный экземпляр TreeSet пустым или нет:
@Test
public void whenCheckingForEmptyTreeSet_shouldCheckForEmpty() {
Set<String> emptyTreeSet = new TreeSet<>();
assertTrue(emptyTreeSet.isEmpty());
}
~ ~~ 9. TreeSet iterator()
Метод 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 first()
Этот метод возвращает первый элемент из TreeSet, если он не пустой. В противном случае выдается исключение NoSuchElementException.
Рассмотрим пример:
@Test
public void whenCheckingFirstElement_shouldReturnFirstElement() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");
assertEquals("First", treeSet.first());
}
11. TreeSet last()
Аналогично приведенному выше примеру, этот метод вернет последний элемент, если набор не пуст:
@Test
public void whenCheckingLastElement_shouldReturnLastElement() {
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add("Last");
assertEquals("Last", treeSet.last());
}
12 .TreeSet subSet()
Этот метод возвращает элементы в диапазоне от 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. TreeSet headSet()
Этот метод вернет элементы 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. TreeSet 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.