文章目錄
-
-
-
- Java集合架構圖
- Map接口簡介
- Map接口的基本操作
- Map接口直接或者間接的實作類
-
- HashMap
- Hashtable
- LinkedHashMap
- TreeMap
- WeakHashMap
- EnumMap
- IdentifyHashMap
- ConcurrentHashMap
- 四種周遊Map接口的方式
-
-
==比較的是位址值,而不是HashCode,是以這裡以後千萬不要掉進誤區了。!!!
Java集合架構圖
點選放大檢視

List , Set, Map都是接口,List , Set繼承至Collection接口(Collection繼承至Iterable),Map為獨立接口
Map接口簡介
- Map不是collection的子接口或者實作類。Map是一個接口。
- Map 的 每個
都持有兩個對象,也就是Entry
,Map 值可以相同,但鍵肯定是唯一的一個鍵一個值
- Map 接口最流行的幾個實作類是
。HashMap、LinkedHashMap、Hashtable 和 TreeMap
(HashMap、TreeMap最常用)
Map接口的基本操作
方法 | 描述 |
---|---|
int size()擷取Map集合大小(即元素數量) | |
boolean isEmpty() | 判斷是否為空 |
boolean containsKey(Object key) | 判斷是否包含某個鍵 |
boolean containsValue(Object value) | 判斷是否包含某個值 |
V get(Object key) | 擷取某個鍵對應的值 |
V put(K key, V value) | 添加鍵值對(K,V) |
V remove(Object key) | 移除某個鍵對應的鍵值對 |
void putAll(Map<? extends K, ? extends V> m) | 添加另一個Map集合 |
void clear() | 清空所有鍵值對 |
Set keySet() | 擷取鍵的集合 |
Collection values() | 擷取值的集合 |
Set<Map.Entry<K, V>> entrySet() | 擷取鍵值對實體的集合 |
interface Entry<K,V> | Map中的内部接口 |
Map接口直接或者間接的實作類
map集合 | 說明 |
---|---|
HashMap | Map基于 的實作。插入和查詢“鍵值對”的開銷是固定的。可以通過構造器設定 和 ,以調整容器的性能。 |
LinkedHashMap | 類似于HashMap,但是疊代周遊它時,取得“鍵值對”的順序是其 ,或者是最近最少使用(LRU)的次序。 。而在 時反而更快,因為它 。 |
TreeMap | 底層是 , ,可用于給Map集合中的 。 |
EnumMap | 集合中的所有 必須是 的執行個體。當EnumMap建立後,會表現成一個數組array,這種表現方式是緊湊高效的。EnumMap的順序,不是由添加時順序決定,而是由枚舉類内部定義的枚舉字段順序決定。 |
HashTable | HashMap是 ,非線程安全的實作他們都實作了map接口,主要差別是HashMap鍵值可以為空null,效率可以高于Hashtable。 |
ConcurrentHashMap | ConcurrentHashMap通常隻被看做 ,用來 其他線程安全的Map容器,比如Hashtable和Collections.synchronizedMap。 |
WeakHashMap | , : 這是為解決特殊問題設計的。如果沒有map之外的引用指向某個“鍵”,則此“鍵”可以被垃圾收集器回收 |
IdentifyHashMap | 使用 hash map |
ArrayMap(安卓) | ArrayMap是一個<key,value>映射的資料結構,它設計上更多的是考慮 , ,一個數組 ,另外一個數組 ,它和SparseArray一樣,也會對 使用 進行 排序,在添加、删除、查找資料的時候都是 得到相應的 ,然後通過index來進行添加、查找、删除等操作,是以,應用場景和SparseArray的一樣,如果在 的情況下,那麼它的性能将退化至少 。 |
SparseArray(安卓) | SparseArray ,在某些條件下性能更好,主要是因為它 了對 (int轉為Integer類型),它 則是通過 來進行資料 的,一個 ,另外一個 ,為了優化性能,它内部對資料還采取了 的方式來表示稀疏數組的資料,進而節約記憶體空間。 |
HashMap
- 底層結構
版本 結構 數組類型 初始化容量 Java1.6/1.7 位桶(數組) + 連結清單 Entry 16 Java1.8 位桶(數組) + 連結清單 + 紅黑樹
)(當連結清單長度超過門檻值 “8” 時,将連結清單轉換為紅黑樹
Node
1.8前使用位桶和連結清單實作
1.8中HashMap是以數組+連結清單+紅黑樹的模式存儲
(當連結清單長度超過門檻值 “8” 時,将連結清單轉換為紅黑樹)
,當同一個hash值(Table上元素)的連結清單節點數不小于8時,将不再以單連結清單的形式存儲了,它是
線程不安全
的Map,并允許使用
Null值
和
Null鍵,
方法上都沒有synchronize關鍵字修飾,具體可以參考HashMap1.7和1.8的差別
Hashtable
- Hashtable是
的一個Map,,它在各個方法上添加了線程安全
。synchronize關鍵字
,因為現在有了但是現在已經不再推薦使用HashTable了
這個專門用于ConcurrentHashMap
下的map實作類,其大大優化了多線程下的性能。多線程場景
-
和key
不允許為空,線程安全,效率低value
LinkedHashMap
- LinkedHashMap底層資料結構是
,哈希表和連結清單
保證鍵哈希表
,唯一
保證連結清單
鍵
,根據有序
存儲插入順序
LinkedHashMap<Integer, String> linkedHashMap= new LinkedHashMap<Integer, String>();
linkedHashMap.put(01, "張三1");
linkedHashMap.put(02, "張三2");
linkedHashMap.put(03, "張三3");
linkedHashMap.put(04, "張三4");
linkedHashMap.put(05, "張三5");
Set<Integer> keys = linkedHashMap.keySet();
for (Integer key : keys) {
System.out.println(key + "|" + linkedHashMap.get(key));
}
執行結果:
TreeMap
- 基于
(Red-Black tree)的 NavigableMap 實作。該映射根據其紅黑樹
進行排序,或者根據建立時提供的鍵的自然順序
進行排序Comparator (内比較器)
- 排序方式類似于TreeSet,分為
和自然排序Comparable
,具體排序取決于在構造器中使用使用比較器比較器排序Comparator
預設自然排序(自然順序,從小到大)
TreeMap<Integer, String> treeMap= new TreeMap<Integer, String>();
treeMap.put(24, "Hello1");
treeMap.put(14, "Hello2");
treeMap.put(34, "Hello3");
treeMap.put(124, "Hello4");
treeMap.put(24, "Hello5");
treeMap.put(24, "Hello6");
treeMap.put(24, "Hello7");
treeMap.put(244, "Hello8");
treeMap.put(624, "Hello9");
treeMap.put(24, "Hello10");
Set<Integer> keys = treeMap.keySet();
for (Integer key : keys) {
String value = treeMap.get(key);
System.out.println(key + "|" + value);
}
執行結果
比較器排序(使用Comparatpr倒序)
TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
treeMap.put(24, "Hello1");
treeMap.put(14, "Hello2");
treeMap.put(34, "Hello3");
treeMap.put(124, "Hello4");
treeMap.put(24, "Hello5");
treeMap.put(24, "Hello6");
treeMap.put(24, "Hello7");
treeMap.put(244, "Hello8");
treeMap.put(624, "Hello9");
treeMap.put(24, "Hello10");
Set<Integer> keys = treeMap.keySet();
for (Integer key : keys) {
String value = treeMap.get(key);
System.out.println(key + "|" + value);
}
執行結果
- TreeMap是NavigableMap接口實作類,NavigableMap接口繼承了SortedMap(繼承了Map)即一個支援排序的map,是以treemap才會支援排序
Treemap四個構造方法
- 無參 預設實作key的自然排序
- 有參 參數為comparator比較器 實作定制排序
- 有參 參數為map 實作key的自然排序
- 有參 參數為map,comparator比較器 實作key的定制排序
自然排序: 要求待添加元素必須實作compareable接口并實作compareTo方法
自定義排序: 要求待添加元素無序實作compareable接口,但建立Treemap對象時必須傳入comparator的比較器,并實作compare方法(匿名内部類)
WeakHashMap
- WeakHashMap 以
實作的,它是弱鍵
都可以是null,的非同步且無序的散清單(hash哈希表)Key和Value
- Map中如果這個Key值指向的對象沒被使用
,此時觸發了GC,該對象就會被回收掉的。(除了自身有對key的引用外,此key沒有其他引用那麼此map會自動丢棄此值,然後被GC回收 )
- HashMap的key保留了對實際對象的強引用,這意味着隻要HashMap對象不被銷毀,還HashMap的所有key所引用的對象就不會被垃圾回收,HashMap也不會自動删除這些key所對應的key-value對;
- WeakHashMap的key隻保留對實際對象的弱引用,這意味着如果WeakHashMap對象的key所引用的對象沒有被其他強引用變量所引用,則這些key所引用的對象可能被垃圾回收,WeakHashMap也可能自動删除這些key所對應的key-value對。
- WeakHashMap中的每個key對象隻持有對實際對象的弱引用,是以,當垃圾回收了該key所對應的實際對象之後,WeakHashMap會自動删除該key對應的key-value對。
- 原理主要是使用的
和WeakReference
實作的,其key就是ReferenceQueue
,而weakReference
中儲存了被回收的 Key-Value。ReferenceQueue
- 如果當其中一個
不再使用被回收時,就将其加入Key-Value
中。當下次再次調用該ReferenceQueue隊列
時,就會去更新該map,比如WeakHashMap
,将其中包含的key-value全部删除掉。這就是所謂的ReferenceQueue中的key-value
。“自動删除”
HashMap和WeakHashMap的差別也在于此,HashMap的key是對實際對象的
強引用
。
- 弱引用(WeakReference)的特性是:當gc線程發現某個對象隻有弱引用指向它,那麼就會将其銷毀并回收記憶體。WeakReference也會被加入到引用隊列queue中。
WeakHashMap<String,String> whm = new WeakHashMap<>();
whm.put(new String("hello1"), "world1");
whm.put(new String("hello2"), "world2");
whm.put(new String("hello3"), "world3");
whm.put("hello4", "world3");
System.out.println(whm);
// System.gc()來通知JVM進行垃圾回收
System.gc();
System.runFinalization();
System.out.println(whm);
執行結果
EnumMap
- EnumMap,該類是專門
的一個集合類。建立EnumMap是必須針對枚舉類設計
,進而将該EnumMap和指定枚舉類關聯起來。指定一個枚舉類
- 該Map在内部以
的形式儲存,是以這種實作形式非常緊湊、高效數組
- EnumMap不允許key為空,value可以為空
- EnumMap的順序,不是由添加時順序決定,而是由枚舉類内部定義的
決定。枚舉字段順序
- 線程不安全,最好在建立的時候調用
方法來進行同步。Collections.synchronizedMap
class TestEnumMap {
public static void main(String[] args) {
//在建立EnumMap時必須顯示或隐式指定它對應的枚舉類
EnumMap<Direction, String> enumMap = new EnumMap<>(Direction.class);
// 所有的key都必須是單個枚舉類的枚舉值
enumMap.put(Direction.UP, "向上移動");
enumMap.put(Direction.DOWN, "向下移動");
enumMap.put(Direction.LEFT, "向左移動");
enumMap.put(Direction.RIGHT, "向右移動");
//EnumMap根據key的自然順序(枚舉值在枚舉類的定義順序)來維護key-value對的順序
for (Map.Entry<Direction, String> entry : enumMap.entrySet())
System.out.println(entry.getKey() + "-----" + entry.getValue());
}
/**
* 内部枚舉類
*/
enum Direction {
UP,
LEFT,
DOWN,
RIGHT;
}
}
執行結果:
插入EnumMap的順序是UP,DOWN,LEFT,RIGHT 但是實際列印順序是 UP,LEFT,DOWN,RIGHT,可以看出 EnumMap的順序是不是由添加時順序決定, 而是有枚舉類中定義的字段順序決定
IdentifyHashMap
IdentityHashMap不是Map的通用實作,它有意違反了Map的正常協定。是一個可以
添加重複key
的Map實作類, 并且
key/value都允許為空
,且是
線程不安全
的
- IdentityHashMap 和 HashMap差別
- IdentifyHashMap和HashMap差不多,唯一的差別就是在
中底層
判斷key的方式不一樣
- HashMap類判斷鍵k1和k2相等的條件為
: 先判斷hashCode是否相同,如果hashCode相同,在用equals判斷值是否相同p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
- IdentifyHashMap判斷k1和k2相等的條件是
:判斷記憶體位址是否相等(item == k)
(上面紅色均為HashMap,IdentityHashMap部分源碼)
- IdentifyHashMap和HashMap差不多,唯一的差別就是在
- 對比HashMap和IdentityHashMap
添加
元素差別
HashMap(java.8)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
可以得出結論
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
1. 通過新key的hashCode()方法,計算出哈希碼,然後從Node數組中找到對應的位置,若為null就放進去。若已經有值了,請看第二步
2. 調用新key的equals()方法去和已經存在的key比較,如果傳回ture 。則視新鍵與已經存在的鍵相同,用新值去更新舊值,然後put方法傳回舊值
3. 若調用equals()傳回false,則認為新鍵和已存在的鍵不一樣,那就會建立一個Node節點,放在此連結清單裡
HashMap的put()方法傳回null的特殊情況:
1. 要是已經存在鍵的映射,但是值是null,那麼調用put()方法再更新鍵的值時, put()方法會把舊值null傳回(因為舊值為null,是以很特殊)
2. 要是找到的位置上沒有鍵的映射,put()方法也是傳回null
IdentityHashMap
public V put(K key, V value) {
final Object k = maskNull(key);
retryAfterResize: for (;;) {
final Object[] tab = table;
final int len = tab.length;
int i = hash(k, len);
for (Object item; (item = tab[i]) != null;
i = nextKeyIndex(i, len)) {
if (item == k) {
@SuppressWarnings("unchecked")
V oldValue = (V) tab[i + 1];
tab[i + 1] = value;
return oldValue;
}
}
final int s = size + 1;
// Use optimized form of 3 * s.
// Next capacity is len, 2 * current capacity.
if (s + (s << 1) > len && resize(len))
continue retryAfterResize;
modCount++;
tab[i] = k;
tab[i + 1] = value;
size = s;
return null;
}
}
可以得出結論
if (item == k) {}
IdentityHashMap比較key值,直接使用的是==,隻要兩個對象的記憶體位址相同即會覆寫舊的key/value
示例代碼
測試對象Demo,用于給HashMap以及IdentityHashMap做key
public class Demo {
private Integer num;
public Demo(Integer num) {
this.num = num;
}
/**
* 重寫了equals方法,隻要值相同就可以認為是同一個對象
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof Demo) {
Demo demo = (Demo) obj;
if (num.equals(demo.num)) {
return true;
}
}
return false;
}
/**
* 重寫hashCode方法,傳回目前num值
* @return
*/
@Override
public int hashCode() {
return num;
}
}
測試HashMap 添加元素
@Test
public void testHashMap() {
// HashMap put方法去重
//通過新key的hashCode()方法,計算出哈希碼,然後從Node數組中找到對應的位置,若為null就放進去。若已經有值了,請看第二步
//調用新key的equals()方法去和已經存在的key比較,如果傳回ture 。則視新鍵與已經存在的鍵相同,用新值去更新舊值,然後put方法傳回舊值
//調用equals()傳回false,則認為新鍵和已存在的鍵不一樣,那就會建立一個Node節點,放在此連結清單裡
Map<String, String> testMap1 = new HashMap<>();
testMap1.put("a", "1");
testMap1.put("a", "2");
testMap1.put("a", "3");
System.out.println(testMap1.size()); //長度1
Map<String, String> testMap2 = new HashMap<>();
//String 底層重寫了hashCode/equals方法,所有值相同的對象都會傳回相同的HashCode并且equals傳回true
testMap2.put(new String("a"), "1");
testMap2.put(new String("a"), "2");
testMap2.put(new String("a"), "3");
System.out.println(testMap2.size()); //長度1
// Integer 底層重寫了hashCode/equals方法,所有值相同的對象都會傳回相同的HashCode并且equals傳回true
Map<Integer, String> testMap3 = new HashMap<>();
testMap3.put(new Integer(200), "1");
testMap3.put(new Integer(200), "2");
testMap3.put(new Integer(200), "3");
System.out.println(testMap3.size()); //長度1
//Demo類 重寫了hashCode/equals方法,所有值相同的對象都會傳回相同的HashCode并且equals傳回true
Demo demo1 = new Demo(1);
Demo demo2 = new Demo(1);
Demo demo3 = new Demo(1);
Map<Demo, String> testMap4 = new HashMap<>();
testMap4.put(demo1, "1");
testMap4.put(demo2, "2");
testMap4.put(demo3, "3");
System.out.println(testMap4.size()); //長度1
}
執行結果
測試IdentityHashMap 添加元素
@Test
public void testIdentityHashMap() {
// IdentityHashMap put方法去重 使用 == 判斷兩個key使用相同, 如果記憶體位址相同覆寫舊key/value
// 常量 "a" 第一次使用時如果常量池中沒有則會添加一個,後續如果在使用會直接使用常量池中已有的變量,是以記憶體位址是指向同一處地方
Map<String, String> testMap1 = new IdentityHashMap<>();
testMap1.put("a", "1");
testMap1.put("a", "2");
testMap1.put("a", "3");
System.out.println(testMap1.size()); //長度1
// 使用 new String() 會在堆中建立一個對象,然後如果常量池沒有會在常量池中建立 "a" ,是以記憶體位址是指向不是同一處地方
Map<String, String> testMap2 = new IdentityHashMap<>();
testMap2.put(new String("a"), "1");
testMap2.put(new String("a"), "2");
testMap2.put(new String("a"), "3");
System.out.println(testMap2.size()); //3
//,new Integer()會直接在堆中建立對象,并将引用指派給棧中,是以記憶體位址是指向不是同一處地方
Map<Integer, String> hashMap2 = new IdentityHashMap<>();
hashMap2.put(new Integer(200), "1");
hashMap2.put(new Integer(200), "2");
hashMap2.put(new Integer(200), "3");
System.out.println(hashMap2.size()); //長度3
// IdentityHashMap底層使用 == 判斷新key與舊key記憶體位址是否一緻,隻有記憶體位址一緻才會覆寫舊key/value
Demo demo1 = new Demo(1);
Demo demo2 = new Demo(1);
Demo demo3 = new Demo(1);
Map<Demo, String> testMap3 = new IdentityHashMap<>();
testMap3.put(demo1, "1");
testMap3.put(demo1, "1");
testMap3.put(demo2, "2");
testMap3.put(demo2, "2");
testMap3.put(demo3, "3");
testMap3.put(demo3, "3");
System.out.println(testMap3.size()); //長度3
}
執行結果:
注意:
- IdentityHashMap重寫了equals和hashcode方法,不過需要注意的是hashCode方法并不是借助Object的hashCode來實作的,而是通過
方法來實作的。System.identityHashCode
- 該類不是線程安全的,如果要使之線程安全,可以調用
方法來實作。Collections.synchronizedMap(new IdentityHashMap(…))
ConcurrentHashMap
- Hashtable之是以效率低下主要是因為其實作使用了
關鍵字對synchronized
等操作進行加鎖,而synchronized關鍵字加鎖是對put
進行加鎖,也就是說在進行put等修改Hash表的操作時,整個對象
,進而使得其表現的效率低下;是以,在鎖住了整個Hash表
版本,Java使用了Java1.5~1.7
機制實作ConcurrentHashMap.分段鎖
- 簡而言之,ConcurrentHashMap在對象中儲存了一個
,預設長度為16。即将整個Segment數組
劃分為Hash表
;而多個分段
Segment元素,即每個分段則類似于一個每個
;這樣,在執行put操作時首先根據Hashtable
到元素hash算法定位
哪個屬于
,然後Segment
即可。是以,ConcurrentHashMap在多線程并發程式設計中可是實作多線程put操作。對該Segment加鎖
(segment有多少個,理論上就可以同時有多少個線程來持有它這個資源。)
- 在JDK1.7之前,ConcurrentHashMap是通過分段鎖機制來實作的,是以其最大并發度受
。是以,在JDK1.8中,ConcurrentHashMap取消了Segment的個數限制
,底層采用與HashMap類似的數組+連結清單+紅黑樹的方式實作,而加鎖則采用CAS和synchronized實作。Segment分段鎖字段
Value和next(這裡注意Node其實就是儲存一個鍵值對的最基本的對象。其中
volatile都是使用的
關鍵字進行了修飾,以確定線程安全。)
四種周遊Map接口的方式
- Entry
- 由于Map中存放的元素均為
,故每一個鍵值對必然存在一個映射關系。鍵值對
- Map中采用
來表示一個鍵值對,鍵值對包含Key和Value (我們總說鍵值對鍵值對, 每一個鍵值對就是一個Entry内部類
)Entry
- Map.Entry裡面包含getKey()和getValue()方法
- 由于Map中存放的元素均為
Iterator<Map.Entry<Integer, Integer>> it=map.entrySet().iterator();
while(it.hasNext()) {
Map.Entry<Integer,Integer> entry=it.next();
int key=entry.getKey();
int value=entry.getValue();
System.out.println(key+" "+value);
}
- entrySet
- entrySet是 java中 鍵-值 對的集合,Set裡面的類型是Map.Entry,一般可以通過map.entrySet()得到。
- entrySet實作了Set接口,裡面存放的是鍵值對。一個K對應一個V。
Set<Map.Entry<String, String>> entryseSet=map.entrySet();
for (Map.Entry<String, String> entry:entryseSet) {
System.out.println(entry.getKey()+","+entry.getValue());
}
- keySet
- keySet是鍵的集合,Set裡面的類型即key的類型
Set<String> set = map.keySet();
for (String s:set) {
System.out.println(s+","+map.get(s));
}
- 四種周遊Map方式對比
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一種:鍵找值方式周遊,普遍使用,二次取值
System.out.println("通過Map.keySet周遊key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二種:周遊entrySet疊代器周遊方式
System.out.println("通過Map.entrySet使用iterator周遊key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三種:周遊entrySet方式,推薦,尤其是容量大時
System.out.println("通過Map.entrySet周遊key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四種:keySet疊代器周遊
Iterator<String> iterator=map.keySet().iterator();
System.out.println("通過Map.keySet使用iterator周遊key和value:");
while (iterator.hasNext()) {
String key=iterator.next();
String value=map.get(key);
System.out.println("key: " + key +",value: "+value);
}
}
推薦使用第三種方式周遊map集合