Java中TreeSet那点事,不是事

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实现。我们看到了它的目的以及它在可用性方面的效率,因为它具有避免重复和排序元素的能力。

image

微信关注:Java知己, 每天更新Java知识哦,期待你的到来!

image

猜你喜欢

转载自blog.csdn.net/feilang00/article/details/86540378