facebook廣告





123

2016年10月22日 星期六

常見Map的特色及用法

來源:http://whu241.blogspot.tw/2013/12/map.html


Map又稱關聯式陣列(Associative Array),為一種使用key-value pair的方式來存取資料的資料結構。在Java Collection Framework中,java.util.Map只是一個Interface,當中定義了一個map所該具有的基本操作;而實作Map介面的類別至少就超過了15個,從這我們也可看出map的重要性了!


不同的實作,由於其針對的面向不一樣,其使用效率及能解決的問題就各自不同,以下筆者描述一些較常見的map class其特點及用法。


HashMap

為以Hash Table為base所發展出來的類別,基本上在使用map時,若無其它考量,則我們應該優先使用HashMap,因其存取資料的時間複雜度可以達到常數時間,非常地快。另外我們也能在HashMap的建構子設定其Capacity,及loading factor。 若我們使用自訂物件作為HashMap的Key,則此時一定要注意是否正確地覆寫了equals()及hashcode(),否則在使用以雜湊函式為基底的類別及函式時,很可能會出現非開發者預期的結果。另外較特別的是HashMap允許鍵值(key)為null喔。



建構函式

[code lang="java"]


//無引數建構子,表採用預設的capacity(16),及load factor(0.75)

Map<String, String> hm0 = new HashMap<String, String> ();


//指定capacity為32,load factor仍為預設的0.75

Map<String, String> hm1 = new HashMap<String, String> (32);


//指定capacity為32,load factor為0.8

Map<String, String> hm2 = new HashMap<String, String> (32,(float) 0.8);

[/code]


不保證順序 

[code lang="java"]

public class MapSorting {

public static void main(String[] args) {

//iterate時,不保證順序

Map<String, String> hm = new HashMap<String, String> ();

hm.put("J", "John");

hm.put("M", "Mary");

hm.put("B", "Bill");

hm.put("C", "Christine");

hm.put("A", "Ariel");


printMap(hm);

}


public static void printMap(Map<String,String> map) {

for (Map.Entry<String,String> entry:map.entrySet()) {

System.out.println(entry.getKey()+" "+entry.getValue());

}

}

}

[/code]


Output從輸出結果中我們可以看到,hashMap輸出key時,並沒有依照插入時的順序,也沒有依照Natural Ordering(M在J前面),所以我們若需要有排序功能的map時,不能選擇HashMap。


[code lang="text"]

A Ariel

B Bill

C Christine

M Mary

J John

[/code]


為了精簡起見,以下的程式碼例子便不再列出main()及printMap函式。


LinkedHashMap

見文生義,LinkedHashMap內部是用linked list來維護其順序性,所以在iterate時其結果乃是依照元素的插入順序或最近最少使用(least-recently-used)順序。在使用上其與hashmap相似,速度只稍差些;但在iterate時卻是比hashmap還來得快喔^^ 而實務上我們也常用其來實作LRU Cache。


[code lang="java"]


//iterate時,保證其順序為插入順序或最近最少使用(least-recently-used,LRU)的順序

Map<String, String> lhm = new LinkedHashMap<String, String> ();

lhm.put("J", "John");

lhm.put("M", "Mary");

lhm.put("B", "Bill");

lhm.put("C", "Christine");

lhm.put("A", "Ariel");


printMap(lhm);


[/code]


Output

我們可以看到輸出結果與元素插入時的順序一致。


[code lang="text"]

J John

M Mary

B Bill

C Christine

A Ariel

[/code]


TreeMap

紅黑樹(red-black tree)的一個實作品,其特點是其key set或key-value pair是有順序性的,而順序為natual ordering或是由所傳入的comparator來決定。另外TreeMap也是唯一一個提供submap()函式的map。


[code lang="java"]

//iterate時,保證其順序為Natural Ordering或Comparator來決定

Map<String, String> tm = new TreeMap<String, String> ();

tm.put("J", "John");

tm.put("M", "Mary");

tm.put("B", "Bill");

tm.put("C", "Christine");

tm.put("A", "Ariel");


printMap(tm);


System.out.println("----------by comparator");

//sort by comparator

tm = new TreeMap<String,String> (new Comparator<String>() {

public int compare(String o1, String o2) {

return o2.compareTo(o1);

}

});

tm.put("J", "John");

tm.put("M", "Mary");

tm.put("B", "Bill");

tm.put("C", "Christine");

tm.put("A", "Ariel");


printMap(tm);


[/code]


Output


[code lang="plain"]

A Ariel

B Bill

C Christine

J John

M Mary

----------by comparator

M Mary

J John

C Christine

B Bill

A Ariel

[/code]



EnumMap

也為Map的一個實作,其特別之處在於只接受列舉(Enumeration)為Key,也因其只接受列舉為key,不像HashMap能接受各種型態的物件作為key,故在實作上能特地為此種情況最佳化。


EnumMap的好處可以從效率上及使用上來描述:技術上,由於EnumMap內部使用Array來實作;另外因不需用呼叫hashcode函式,故其也不會產生collision的問題;所以在同是key為enum的情況下,EnumMap的效能是好過HashMap的。而在使用上,以列舉作為key便不怕有打錯字的情況了,這個特性,筆者非常地喜歡!



特色:

1. 不接受null為key。

2. 以Natural Ordering的方式來儲存Key。

3. 效能比HashMap稍好些。


[code lang="java"]


import java.util.EnumMap;

import java.util.Map;


enum Keys {

A(1), B(2), C(3), J(4), M(5);


private int code;


private Keys(int code) {

this.code = code;

}


public int getCode() {

return this.code;

}

}


ublic class EnumTest {

public static void main(String[] args) {

Map<Keys, String> enumMap = new EnumMap<Keys, String> (Keys.class);

enumMap.put(Keys.J, "John");

enumMap.put(Keys.M, "Mary");

enumMap.put(Keys.B, "Bill");

enumMap.put(Keys.C, "Christine");

enumMap.put(Keys.A, "Ariel");




for (Map.Entry<Keys, String> entry: enumMap.entrySet()) {

System.out.println(entry.getKey()+" "+entry.getKey().getCode()+" "+entry.getValue());

}

}

}


[/code]


Output
我們可看到輸出的順序為key按照Natural Ordering的排序。


[code lang="plain"]

A 1 Ariel

B 2 Bill

C 3 Christine

J 4 John

M 5 Mary

[/code]


WeakHashMap

WeakHashMap在設計上使用canonicalized mappings來節省儲存空間,而其也能讓GC自動地回收key-value pair,讓使用者不用自行清理。 一般而言,一個物件若有reference指向它時,其是不會被GC回收掉的。如hashMap.put("a",Object A),由於key "a"指向Object A,所以就算key a己沒有被其它程式使用到,key "a"及其value仍不會被回收掉,開發者需手動呼叫remove(),才能避免空間的浪費。而WeakHashMap中的的key若沒被其它程式reference時,這對key-value pair便會自動被GC回收掉。


[code lang="java"]

import java.util.HashMap;

import java.util.Map;

import java.util.WeakHashMap;


public class WeakHashMapTest {

public static void main(String[] args) {

String hashKey = new String("5566");

String weakHashKey = new String("8899");


ap<String,String> hashMap = new HashMap<String,String>();

Map<String,String> weakHashMap = new WeakHashMap<String,String>();



hashMap.put(hashKey, "I'm sad!");

weakHashMap.put(weakHashKey, "Apple");


System.out.printf("value in HashMap: %s \n",hashMap.get(hashKey));

System.out.printf("value in WeakHashMap: %s \n",weakHashMap.get(weakHashKey));


//將兩個map的鍵值設為null

hashKey = null;

weakHashKey = null;


//建議啟動Garbage Collection

System.gc();


System.out.println("-----after Garbage Collection------");

System.out.printf(" HashMap--> size: %d , ",hashMap.size());

for (String str:hashMap.values()){

System.out.printf("value: %s \n",str);

}


System.out.printf("WeakHashMap--> size: %d , ",weakHashMap.size());

for (String str:weakHashMap.values()) {

System.out.printf(" value: %s \n", str); //key-value entry己被清掉,迴圈己進不來了

}

}

}

[/code]




Output


[code lang="plain"]

value in HashMap: I'm sad!

value in WeakHashMap: Apple

-----after Garbage Collection------

HashMap--> size: 1 , value: I'm sad!

WeakHashMap--> size: 0 ,

[/code]



ConcurrentHashMap
雖然HashMap很好用,但是其並非thread-safe的。當有多個執行緒同時對HashMap進行讀取及修改的動作時,便可能產生「ConcurrentModificationException」;而在JDK 1.5之前,只有HashTable及利用Collections.synchronizedMap()才能保証map在多執行緒環境下的安全,但這兩個方式採用的方法為鎖住整個map,這會造成效能的顯著下降。所以在Sun在1.5時加入了ConcurrentHashMap類別,它能保證thread-safe且效能也不錯。嗯這是怎麼做到的呢?讓我們繼續看下去^^ (為了方便,以下用chm來簡稱ConcurrentHashMap)


為了讓同一個map可以被很多個執行緒同時又讀又寫,chm的作法是只鎖住部份的map!其藉由使用ConcurrencyLevel的值(可在建構子中指定,預設值為16)來將map切成很多塊,。每一塊皆能由不同的執行緒來使用,所以使用不同塊的執行緒並不會互相干擾,而用到同一塊map的執行緒們的溝通則還是藉由lock的機制來保護。



特色

1. 不會丟出ConcurrentModificationException.

2. 有個特別的putIfAbsent(key, value)函式。

3. 不允許鍵值為null。




多執行緒同時存取HashMap

[code lang="java"]

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

import java.util.Map.Entry;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;



public class NotThreadSafeMap implements Runnable {

private String name;

private static Map<Integer, String> map = new HashMap<Integer,String>();



public NotThreadSafeMap(Integer number, String name) {

this.name = name;

map.put(number, name);

}



public void run() {

try{

Iterator<Integer> it = map.keySet().iterator();

while(it.hasNext()) {

Integer key = it.next();

map.put(key+1, name);

}

System.out.println(name+ " inserted");

} catch(Exception e) {

e.printStackTrace();

} finally {

}

}



public static void main(String[] args) {

NotThreadSafeMap not1 = new NotThreadSafeMap(1,"Apple");

NotThreadSafeMap not2 = new NotThreadSafeMap(2,"Beagle");



ExecutorService executor = Executors.newCachedThreadPool();

executor.execute(not1);

executor.execute(not2);



for (Entry<Integer, String> entry: map.entrySet()) {

System.out.println("Key:" + entry.getKey() + " Value:" + entry.getValue());

}

executor.shutdownNow();

}

}

[/code]



Output
產生了exception@@



[code lang="plain"]

Key:1 Value:Apple

Beagle inserted

Apple inserted

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.HashMap$HashIterator.nextEntry(HashMap.java:894)

at java.util.HashMap$EntryIterator.next(HashMap.java:934)

at java.util.HashMap$EntryIterator.next(HashMap.java:932)

at fun.practice.map.NotThreadSafeMap.main(NotThreadSafeMap.java:41)

[/code]



多執行緒同時存取CHM



[code lang="java"]

import java.util.Iterator;

import java.util.Map;

import java.util.Map.Entry;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;



public class ThreadSafeMap implements Runnable {

private String name;

private static Map<Integer, String> map = new ConcurrentHashMap<Integer,String>();



public ThreadSafeMap(Integer number, String name) {

this.name = name;

map.put(number, name);

}



public void run() {

try{

Iterator<Integer> it = map.keySet().iterator();

while(it.hasNext()) {

Integer key = it.next();

map.put(key+1, name);

}

System.out.println(name+ " inserted");

} catch(Exception e) {

e.printStackTrace();

} finally{

}

}



public static void main(String[] args) {

ThreadSafeMap not1 = new ThreadSafeMap(1,"Apple");

ThreadSafeMap not2 = new ThreadSafeMap(2,"Beagle");



ExecutorService executor = Executors.newCachedThreadPool();

executor.execute(not1);

executor.execute(not2);



for (Entry<Integer, String> entry: map.entrySet()) {

System.out.println("Key:" + entry.getKey() + " Value:" + entry.getValue());

}

executor.shutdownNow();

}

}

[/code]



Output

結果正常,不會產生Exception。



[code lang="lang"]

Apple inserted

Key:2 Value:Apple

Key:1 Value:Apple

Key:3 Value:Beagle

Key:4 Value:Beagle

Beagle inserted

[/code]



IdentityHashMap

HashMap在判斷一個鍵值(key)是否己存在時,會呼叫key物件的equals()方法來辨別,任何物件預設的equals()方法都是用物件預設的reference來比較,被覆寫後就是依照此物件的邏輯來比了。IdentityHashMap則是使用"==",也就是利用reference來比較兩個物件是否相等,不管此物件的equals()有無被覆寫;而其是利用System.identityHashCode(object)來產生hashcode,所以也不會受到mutable object的影響。


IdentityHashMap之所以會存在主要是為了解決使用可變物件(mutable object)為key時,hashmap可能會遇到的困擾。如下例中筆者自行定義了一個Person物件,有name及age兩個屬性。另外筆者覆寫了equals(),以name及age來判斷兩個person物件是否一樣;程式一開始用hashmap來存放person與職稱的對應,在放入了一個person物件為key後,我們改變了此person物件的age,由於age會被equals()使用到,當呼叫containsKey()時其結果會變得不可預測,另外此例中age也會影響到hashcode的計算,所以當拿改變過後的person物件,想取出對應的職稱時,hashmap也可能會找不到原本其應該mapping到的value。


而若我們使用IdentityHashMap來存mutable object時,不管此物件在被放入map之後經過了多少的改變,由於是使用reference來判斷key是否相等,所以containsKey()傳回來的結果總是一致的,另外在getValue(key)時,其傳回的hashCode也不會收到物件改變的影響。



[code lang="java"]



import java.util.HashMap;

import java.util.IdentityHashMap;

import java.util.Map;

import static java.lang.System.out;



class Person {

private String name;

private Integer age;



public Person (String name,Integer age) {

this.name = name;

this.age = age;

}



public void setName(String name) {

this.name = name;

}



public void setAge(Integer age) {

this.age = age;

}



@Override

public boolean equals(Object obj) {

if (obj instanceof Person) {

Person person2 = (Person) obj;

if (this.name.equals(person2.name) && this.age == person2.age) {

return true;

}

}




return false;

}



@Override

public int hashCode() {

return this.age * 47;

}



}



public class IdentityMapTest {

public static void main(String... args) {

Person p = new Person("Wallace",31);



out.println("Mutable object in Hashmap:");



Map<Person, String> hashMap = new HashMap<Person, String>();

hashMap.put(p, "Engineer");



out.println("titleBeforeChange:"+hashMap.get(p));

p.setAge(99);

out.println("title1AfterChange:"+hashMap.get(p));



out.println("\nMutable object in IdentityHashmap:");

p = new Person("Wallace",31);



Map<Person, String> iHashMap = new IdentityHashMap<Person, String>();

iHashMap.put(p, "Engineer");




out.println("titleBeforeChange:"+iHashMap.get(p));

p.setAge(99);

out.println("title1AfterChange:"+iHashMap.get(p));



}

}



[/code]



Output

我們可以看到第3行的取出結果為null,這便是問題所在了。



[code lang="plain"]



Mutable object in Hashmap:

titleBeforeChange:Engineer

title1AfterChange:null



Mutable object in IdentityHashmap:

titleBeforeChange:Engineer

title1AfterChange:Engineer



[/code]



Reference

1. Thinking In Java, 4e.

2. http://java.dzone.com/articles/difference-between-hashmap-and