橫掃Java Collections系列 —— TreeSet
簡言之,TreeSet 是一個繼承AbstractSet 類的有序集合類,實現了NavigableSet 介面,該介面中提供了針對給定搜尋目標返回最接近匹配項的系列導航方法。主要有以下特點:
- 其中儲存的元素具有唯一性
- 不保證元素的插入順序
- 對元素進行升序排序
- 非執行緒安全
在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
。該方法約定,對於某元素而言,只有在集合中不存在相同元素時才可以新增。
讓我們向TreeSet 中加入一個元素:
@Test public void AddingElement() { Set<String> treeSet = new TreeSet<>(); assertTrue(treeSet.add("String Added")); } 複製程式碼
add()
方法非常重要,因為該方法的實現細節說明了TreeSet
的內部實現原理,即利用TreeMap
的put
方法來儲存元素:
public boolean add(E e) { return m.put(e, PRESENT) == null; } 複製程式碼
程式碼中的變數m
指向內部的一個TreeMap
例項(注意TreeMap
實現了NavigateableMap
介面)。因此,當TreeSet
內部依賴於一個NavigableMap
,當建立一個TreeSet
例項時,內部會通過一個TreeMap
例項進行初始化:
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()
該方法接受fromElement 和toElement 兩個引數,並返回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
的效能稍低些。add
、remove
、search
等操作時間複雜度為O(log n)
,按照儲存順序列印n個元素則耗時為O(n)
。
如果我們想要按序儲存條目,並且按照升序或者降序對集合進行訪問和遍歷,那麼TreeSet 就應該作為首選集合。升序方式的操作與檢視效能要強於降序方式。
區域性性原則——是一個術語,表示根據記憶體訪問模式頻繁訪問相同值或者相關的儲存位置。
當我們說區域性性時,表明:
- 相似的資料通常會被程式以相近的頻率訪問
- 如果兩個條目按照給定順序接近,TreeSet 會在資料結構中將這兩個元素放在相近的位置,記憶體中也同樣。
TreeSet 作為一個有著更強區域性性特點的資料結構,我們可以根據區域性性原理得出結論,如果記憶體不足並且需要訪問自然順序相對接近的元素,那我們就應該優先考慮TreeSet 。如果需要從硬碟中讀取資料,因為硬碟讀取的延時大大超過快取與記憶體讀取,因此TreeSet 更加適合,因為其有著更好的區域性性。