1.概述
在本文中,我们将介绍Java Collections Framework的一个组成部分,以及最受欢迎的Set实现之一 TreeSet。
2. TreeSet简介
简而言之,TreeSet是一个有序集合,它扩展了AbstractSet类并实现了NavigableSet接口。
以下是此实现最重要方面的快速摘要:
- 它存储唯一的元素
- 它不保留元素的插入顺序
- 它按升序对元素进行排序
- 它不是线程安全的
在该实现中,对象根据其自然顺序以升序排序和存储。该TreeSet中使用平衡树,更具体的一个红黑树。
简单地说,作为自平衡二叉搜索树,二叉树的每个节点包括一个额外的位,用于识别红色或黑色的节点的颜色。在随后的插入和删除期间,这些“颜色”位有助于确保树保持或多或少的平衡。
让我们创建一个TreeSet的实例:
Set<String> treeSet = new TreeSet<>();
2.1 TreeSet使用Comparator构造函数
我们可以构造一个带有构造函数的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"));
}
该添加方法是非常重要的,因为该方法的实现细节说明了如何TreeSet的内部工作,它如何利用TreeMap中的 放方法来存储元素:
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
变量m指的是内部支持TreeMap(注意TreeMap实现了NavigateableMap):
private transient NavigableMap<E, Object> m;
因此,TreeSet在内部依赖于后备NavigableMap,当创建TreeSet的实例时,它会使用TreeMap实例进行初始化:
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使我们能够按降序迭代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被创建之后,只能通过迭代器remove()方法。其它任何方式在集合上删除元素都将抛出ConcurrentModificationException异常。
让我们为此创建一个测试:
@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");
}
}
或者,如果我们使用了迭代器的remove方法,那么我们就不会遇到异常:
@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());
}
不能保证迭代器的 fail-fast 事件行为,因为在存在不同步的并发修改时不可能做出任何硬性保证。
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
10. TreeSet first()
如果TreeSet不为空,则此方法返回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添加元素时,元素将根据其自然顺序或比较器指定的方式进行排序。因此,与现有元素相比,添加null会导致NullPointerException,因为null无法与任何值进行比较:
@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)的时间,而像打印操作ñ在有序元素需要O(n)的时间。
局部性原则 - 是根据存储器访问模式,经常访问相同值或相关存储位置的现象的术语。
当我们访问时:
- 类似数据通常由具有相似频率的应用程序访问
- 如果给定排序附近有两个条目,则TreeSet将它们放置在数据结构中彼此靠近,因此在内存中
我们可以说一个TreeSet的数据结构有更大的地方,因此,得出结论:按照局部性原理,我们应该优先考虑一个TreeSet的,如果我们短期使用,我们想访问相对靠近元素根据他们的自然顺序相互依赖。
如果需要从硬盘驱动器读取数据(其延迟大于从缓存或内存中读取的数据),则更喜欢TreeSet,因为它具有更大的局部性
17.结论
在本文中,我们将重点介绍如何在Java中使用标准TreeSet实现。我们看到了它的目的以及它在可用性方面的效率,因为它具有避免重复和排序元素的能力。