1. Обзор
В этой статье мы рассмотрим неотъемлемую часть Java Collections Framework и одну из самых популярных реализаций Set - TreeSet .
2. Введение в TreeSet
Проще говоря, TreeSet это отсортированная коллекция, которая расширяет класс AbstractSet и реализует интерфейс NavigableSet
Вот краткий обзор наиболее важных аспектов этой реализации:
-
Хранит уникальные элементы
-
Не сохраняет порядок вставки элементов
-
Сортирует элементы в порядке возрастания
-
Это не потокобезопасный
-
В этой реализации объекты сортируются и хранятся в порядке возрастания в соответствии с их естественным порядком ** . TreeSet использует самобалансирующееся двоичное дерево поиска, более конкретно https://en.wikipedia.org/wiki/Red%E2%80%93black tree[a Red-Black__ tree].
Проще говоря, будучи самобалансирующимся двоичным деревом поиска, каждый узел двоичного дерева содержит дополнительный бит, который используется для определения цвета узла, который является красным или черным. Во время последующих вставок и удалений эти «цветные» биты помогают обеспечить более или менее сбалансированное дерево.
Итак, давайте создадим экземпляр 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 ()
Как и ожидалось, метод add () можно использовать для добавления элементов в TreeSet . Если элемент был добавлен, метод возвращает true, в противном случае - false.
-
В контракте метода указано, что элемент будет добавлен только тогда, когда того же самого еще нет в Set . **
Давайте добавим элемент в TreeSet :
@Test
public void whenAddingElement__shouldAddElement() {
Set<String> treeSet = new TreeSet<>();
assertTrue(treeSet.add("String Added"));
}
-
Метод add чрезвычайно важен, так как детали реализации метода иллюстрируют внутреннюю работу TreeSet ** , как он использует метод TreeMap’s put для хранения элементов:
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>());
}
Подробнее об этом можно узнать по ссылке:/java-treemap[эта статья].
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 ()
-
Метод 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 () возвращает итератор, повторяющийся в возрастающем порядке по элементам Set. Эти итераторы работают быстро .
Мы можем наблюдать восходящий порядок итераций здесь:
@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 позволяет нам перебирать Set в порядке убывания.
Давайте посмотрим, что в действии:
@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());
}
}
-
Iterator генерирует исключение _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());
}
-
Нет никаких гарантий относительно отказоустойчивого поведения итератора, поскольку невозможно сделать какие-либо жесткие гарантии при наличии несинхронизированной параллельной модификации. **
Подробнее об этом можно узнать по ссылке:/java-fail-safe-vs-fail-fast-iterator[здесь].
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. Хранение Null элементов
-
До Java 7 можно было добавлять элементы null в пустой TreeSet. **
Однако это считалось ошибкой. Поэтому TreeSet больше не поддерживает добавление null .
Когда мы добавляем элементы в TreeSet, элементы сортируются в соответствии с их естественным порядком или в соответствии с указаниями comparator. Следовательно, добавление null, по сравнению с существующими элементами, приводит к NullPointerException , поскольку null нельзя сравнивать с любым значением :
@Test(expected = NullPointerException.class)
public void whenAddingNullToNonEmptyTreeSet__shouldThrowException() {
Set<String> treeSet = new TreeSet<>();
treeSet.add("First");
treeSet.add(null);
}
Элементы, вставленные в TreeSet , должны либо реализовывать интерфейс Comparable , либо, по крайней мере, быть приняты указанным компаратором. Все такие элементы должны быть взаимно сопоставимы, i.e. 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 находится на нижней стороне. Такие операции, как add , remove и search , занимают O (log n) время, в то время как такие операции, как печать n элементов в отсортированном порядке, требуют O (n) время.
TreeSet должен быть нашим основным выбором, если мы хотим, чтобы наши записи сортировались как TreeSet , к которым можно обращаться и проходить в порядке возрастания или убывания, а выполнение восходящих операций и представлений, вероятно, будет быстрее, чем у нисходящих.
Принцип локальности - это термин, обозначающий явление, при котором часто обращаются к одним и тем же значениям или связанным местам хранения в зависимости от схемы доступа к памяти.
Когда мы говорим, местность:
-
Подобные данные часто доступны из приложения с аналогичными
частота ** Если две записи находятся рядом с указанием порядка, TreeSet размещает их
рядом друг с другом в структуре данных, и, следовательно, в памяти
TreeSet является структурой данных с большей локальностью, поэтому мы можем сделать вывод в соответствии с Принципом локальности, что мы должны отдавать предпочтение TreeSet , если у нас мало памяти и если мы хотим получить доступ к элементам, которые находятся относительно близко друг другу в соответствии с их естественным порядком.
В случае, если данные должны быть прочитаны с жесткого диска (который имеет большую задержку, чем данные, прочитанные из кеша или памяти), тогда предпочитайте TreeSet , поскольку он имеет большую локальность
17. Заключение
В этой статье мы сосредоточимся на понимании того, как использовать стандартную реализацию TreeSet в Java. Мы увидели его назначение и эффективность в отношении удобства использования, учитывая его способность избегать дублирования и сортировки элементов.
Как всегда, фрагменты кода можно найти over на GitHub .