橫掃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更加適合,由於其有着更好的局部性。

相關文章
相關標籤/搜索