横扫Java Collections系列 —— TreeSet

介绍

简言之,TreeSet是一个继承AbstractSet类的有序集合类,实现了NavigableSet接口,该接口中提供了针对给定搜索目标返回最接近匹配项的系列导航方法。主要有如下特色:java

  • 其中保存的元素具备惟一性
  • 不保证元素的插入顺序
  • 对元素进行升序排序
  • 非线程安全

TreeSet中,元素按照其天然序升序排列和存储,内部使用了一种自平衡二叉搜索树,也就是红黑树。红黑树做为自平衡二叉搜索树,其中每一个节点都额外保有一个比特,用来指示当前的节点颜色是红色或者黑色。这些“颜色”比特在后续的插入或者删除中,有助于确保树结构保持平衡。缓存

建立TreeSet实例很简单:安全

Set<String> treeSet = new TreeSet<>();
复制代码

此外,TreeSet还提供了一个有参构造器,能够传入一个Comparable或者Comparator参数,该比较器会决定集合中元素排列的顺序:数据结构

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

尽管TreeSet不是线程安全的容器,可是能够调用Collections.synchronizedSet()装饰方法使其同步化:并发

Set<String> syncTreeSet = Collections.synchronizedSet(treeSet);
复制代码

经常使用方法

知道了如何建立TreeSet实例以后,接着看一下TreeSet中经常使用的操做。性能

add()

顾名思义,add()方法能够向TreeSet集合中添加元素,若是元素添加成功,则返回true,不然返回false。该方法约定,对于某元素而言,只有在集合中不存在相同元素时才能够添加。this

让咱们向TreeSet中加入一个元素:spa

@Test
public void AddingElement() {
    Set<String> treeSet = new TreeSet<>();
 
    assertTrue(treeSet.add("String Added"));
 }
复制代码

add()方法很是重要,由于该方法的实现细节说明了TreeSet的内部实现原理,即利用TreeMapput方法来保存元素:线程

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

代码中的变量m指向内部的一个TreeMap实例(注意TreeMap实现了NavigateableMap接口)。所以,当TreeSet内部依赖于一个NavigableMap,当建立一个TreeSet实例时,内部会经过一个TreeMap实例进行初始化:code

public TreeSet() {
    this(new TreeMap<E,Object>());
}
复制代码

contains()

contain()方法可用于检查给定TreeSet中是否包含某特定元素,若是包含则返回true,不然返回false

用法很简单:

@Test
public void CheckingForElement() {
    Set<String> treeSetContains = new TreeSet<>();
    treeSetContains.add("String Added");
 
    assertTrue(treeSetContains.contains("String Added"));
}
复制代码

remove()

remove()方法用于删除集合中的特定元素,若是集合中包含该特定元素,该方法会返回true

用法以下:

@Test
public void RemovingElement() {
    Set<String> removeFromTreeSet = new TreeSet<>();
    removeFromTreeSet.add("String Added");
 
    assertTrue(removeFromTreeSet.remove("String Added"));
}
复制代码

clear()

若是想要清除集合中的全部元素,可使用clear()方法:

@Test
public void ClearingTreeSet() {
    Set<String> clearTreeSet = new TreeSet<>();
    clearTreeSet.add("String Added");
    clearTreeSet.clear();
  
    assertTrue(clearTreeSet.isEmpty());
}
复制代码

size()

size()方法能够获得TreeSet中元素的个数,该方法也是Set API中的基本方法之一:

@Test
public void CheckTheSizeOfTreeSet() {
    Set<String> treeSetSize = new TreeSet<>();
    treeSetSize.add("String Added");
  
    assertEquals(1, treeSetSize.size());
}
复制代码

isEmpty()

isEmpty()方法可用于验证给定的TreeSet实例是否为空:

@Test
public void CheckEmptyTreeSet() {
    Set<String> emptyTreeSet = new TreeSet<>();
     
    assertTrue(emptyTreeSet.isEmpty());
}
复制代码

first()

若是TreeSet不为空,该方法会返回其中的第一个元素,不然会抛出NoSUchElementException异常。示例以下:

@Test
public void GetFirstElement() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    
    assertEquals("First", treeSet.first());
}
复制代码

last()

与上面的方法相似,若是TreeSet不为空,该方法将返回其中的最后一个元素:

@Test
public void GetLastElement() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Last");
     
    assertEquals("Last", treeSet.last());
}
复制代码

subSet()

该方法接受fromElementtoElement两个参数,并返回TreeeSet中这两个参数指定索引范围之间的全部元素。注意,该区间中包括fromElement,不包括toElement

@Test
public void UseSubSet() {
    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);
}
复制代码

headSet()

该方法会返回TreeSet中小于指定项的全部元素:

@Test
public void UseHeadSet() {
    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));
}
复制代码

tailSet()

该方法返回TreeSet中大于或等于指定项的全部元素。

@Test
public void UseTailSet() {
    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));
}
复制代码

Iterator()

Iterator()方法会返回一个按照升序对集合中的元素进行迭代的迭代器,且该迭代器具备快速失败机制。

升序迭代以下:

@Test
public void IterateTreeSetInAscendingOrder() {
    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 IterateTreeSetInDescendingOrder() {
    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());
    }
}
复制代码

若是迭代器已经建立,而且集合被除迭代器的remove()方法以外的其它方式进行修改,迭代器将会抛出ConcurrentModificationException

示例以下:

@Test(expected = ConcurrentModificationException.class)
public void ModifyingTreeSetWhileIterating() {
    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 RemovingElementUsingIterator() {
  
    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());
}
复制代码

TreeSet没法对迭代器的快速失败机制做出保证,由于在未同步的并发修改场景中,没法做出任何硬性保证。

Null元素的存储

在Java 7以前,用户能够向空TreeSet对象中添加null。可是,这个被当作了一个bug,所以在后续的版本中再也不支持null值的添加。

当咱们向TreeSet中添加元素时,其中的元素会按照天然序或者指定的comparator来进行排序。因为null不能与任何值做比较,所以当向TreeSet中添加null时,null与已有元素作比较时,会抛出NullPointerException

@Test(expected = NullPointerException.class)
public void AddNullToNonEmptyTreeSet() {
    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 UsingComparator() {
    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);
}
复制代码

TreeSet性能

HashSet相比,TreeSet的性能稍低些。addremovesearch等操做时间复杂度为O(log n),按照存储顺序打印n个元素则耗时为O(n)

若是咱们想要按序保存条目,而且按照升序或者降序对集合进行访问和遍历,那么TreeSet就应该做为首选集合。升序方式的操做与视图性能要强于降序方式。

局部性原则——是一个术语,表示根据内存访问模式频繁访问相同值或者相关的存储位置。

当咱们说局部性时,代表:

  • 类似的数据一般会被程序以相近的频率访问
  • 若是两个条目按照给定顺序接近,TreeSet会在数据结构中将这两个元素放在相近的位置,内存中也一样。

TreeSet做为一个有着更强局部性特色的数据结构,咱们能够根据局部性原理得出结论,若是内存不足而且须要访问天然顺序相对接近的元素,那咱们就应该优先考虑TreeSet。若是须要从硬盘中读取数据,由于硬盘读取的延时大大超过缓存与内存读取,所以TreeSet更加适合,由于其有着更好的局部性。

相关文章
相关标签/搜索