如何用Map物件建立Set物件

TZQ_DO_Dreamer發表於2014-11-12

Java中的Map和Set有不少相似之處。本文將分享一個把Map類轉化成Set類的小技巧。

或許你已經知道,HashSet其實是一個披著Set方法外衣的HashMap;同樣,TreeSet其實也是一個披著Set方法外衣的TreeMap。Map並不支援直接用迭代器進行遍歷,因此下面的這段程式碼編譯無法通過:

1
2
3
Map<String, Double> salaries = newHashMap<>();
for(doublesalary : salaries) { // does not compile
}

我們可以通過遍歷Map中的key集合、value集合和entry集合來實現Map的遍歷。由於Map中的value是可以重複出現的,因此values()方法返回的是一個Collection型別的集合。而Map中的key是不允許重複的,因此keySet()方法和entrySet()返回的都是Set型別的集合。

因此,我們可以採用下面的方法來遍歷Map:

1
2
3
Map<String, Double> salaries = newHashMap<>();
for(doublesalary : salaries.values()) {
}

或者可以通過遍歷key來遍歷Map:

1
2
3
Map<String, Double> salaries = newHashMap<>();
for(String name : salaries.keySet()) {
}

當然,還可以通過遍歷entry來遍歷Map:

1
2
3
4
5
Map<String, Double> salaries = newHashMap<>();
for(Map.Entry<String, Double> entry : salaries.entrySet()) {
  String name = entry.getKey();
  doublesalary = entry.getValue();
}

我經常看到程式設計師這樣遍歷Map:先獲取keySet,然後對keys進行遍歷,並通過get()方法找到對應的value。

1
2
3
4
Map<String, Double> salaries = newHashMap<>();
for(String name : salaries.keySet()) { // less efficient way to
    doublesalary = salaries.get(name);   // iterate over entries
}

從直觀上看,採用遍歷entry的方式遍歷Map會更加高效一些,這種遍歷方式的時間複雜度是O(n)。然而,如果HashMap中的元素分佈均勻,呼叫get()方法查詢元素的時間複雜度將是O(1),那麼這兩種方法遍歷HashMap的時間複雜度是一樣的,都是O(n)。這兩種遍歷方式雖然有所不同,但時間複雜度都是線性的。但這個結論並不適用於其它型別的Map,特別是TreeMap。TreeMap的平均查詢效率是O(log n),因此通過keySet遍歷TreeMap的時間複雜度是O(n x log n)。

java.util包中有很多Map類,其中一些Map類有著對應型別的Set類實現,例如TreeMap和HashMap。這些Set類都是基於對應的Map類實現的,因此它們和對應的Map類保持相同的演算法複雜度以及併發特性。

本文的重點來了。我在完成併發專修課程中的某道練習題時,需要一個快速高效並且執行緒安全的HashSet。起初,我直接把ConcurrentHashMap當作Set用,把要插入Set的元素以Key的形式插入Map,Key所對應的Value則是一個無意義的預設值。後來我發現,Java 6中的java.util.Collections類提供了一個newSetFromMap()方法,該方法能夠基於指定的Map物件建立一個新的Set物件。在建立這個Map<K, V>物件時,K的資料型別必須與你想要建立的Set中元素的資料型別一致;而V必須是Boolean型別的,這是因為value欄位用於標記該元素是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
importjava.util.*;
importjava.util.concurrent.*;
 
publicclass ConcurrentSetTest {
    publicstatic void main(String[] args) {
        Set<String> names = Collections.newSetFromMap(
            newConcurrentHashMap<String, Boolean>()
        );
        names.add("Brian Goetz");
        names.add("Victor Grazi");
        names.add("Heinz Kabutz");
        names.add("Brian Goetz");
        System.out.println("names = " + names);
    }
}

當然,newSetFromMap()方法只能返回標準Set介面型別的物件。如果你的Map類有著更豐富的介面(與標準Map<K, V>介面相比),你還是需要自行封裝實現對應的Set類。

希望讀者能從本文中有所收穫。如果你曾經為找不到ConcurrentHashSet而煩惱,現在你就可以自己建立一個了。

相關文章