1. 程式人生 > >java資料結構之Map

java資料結構之Map

Java中的List和Map是使用頻率非常高的資料結構。這裡介紹下常用的Map實現。
先來看下類圖:

HashMap的實現原理

HashMap與HashTable的異同

HashTable與HashMap的儲存機制基本相同,都是採用陣列+連結串列。
不同點:
1.HashMap是非執行緒安全的,HashTable是執行緒安全的。它的大多數方法都加了synchronized。
2.HashMap允許key和value的值為null,HashTable不允許null值的存在。在HashTable的put方法中,如果V為null,直接丟擲NullPointerException。
3.因為HashTable加了同步處理,所以HashMap效率高於HashTable。

HashTable與ConcurrentHashMap有何不同?

HashTable與ConcurrentHashMap都是執行緒安全的Map,有何不同?
HashTable使用的synchronized對方法加鎖,實際鎖住的是整個物件;而ConcurrentHashMap使用的是lock,這樣在操作的時候鎖住的不是這個物件。
而且ConcurrentHashMap採用了分段鎖的設計,只有在同一個分段內才存在競態關係,不同的分段鎖之間沒有鎖競爭。相比於對整個Map加鎖的設計,分段鎖大大的提高了高併發環境下的處理能力。

ConcurrentHashMap使用多個子Hash表,即Segment。每個Segment是一個子Hash表。

ConcurrentHashMap要避免呼叫size()和containsValue()方法,會對整個Map進行掃描。

LinkedHashMap——有序的HashMap

LinkedHashMap繼承自HashMap,在HashMap的基礎上增加了一個連結串列,用以存放元素的順序。

LinkedHashMap提供2種類型的順序:一是元素插入時的順序;二是最近訪問的順序。
可以通過下面的建構函式指定排序行為:

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean
accessOrder) { }

accessOrder為true時表示按照元素訪問時間排序;為false時表示按照元素插入的順序排序。預設是false。
示例程式碼:

Map<String,String> map = new LinkedHashMap<>(16,0.75f,false);
map.put("hello","hello");
map.put("world","world");
map.put("!","!");
for (Iterator<String> iterator = map.keySet().iterator();iterator.hasNext();) {
    String key = iterator.next();
    System.out.println(key + "->" + map.get(key));
}

輸出如下:

hello->hello
world->world
!->!

按照元素插入的順序輸出。
如果把上面的建構函式的最後一個引數accessOrder改為true,則在迭代器遍歷時會報錯。

Map<String,String> map = new LinkedHashMap<>(16,0.75f,true);

錯誤資訊:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:390)
    at java.util.LinkedHashMap$KeyIterator.next(LinkedHashMap.java:401)
    at com.tommy.core.test.Test.main(Test.java:54)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

ConcurrentModificationException異常一般是在對集合迭代的過程中被修改時丟擲。不僅僅是LinkedHashMap,所有的集合都不允許在迭代器中修改集合的結構。

因為我們將accessOrder改為true,表示按照最後訪問的時間排序。在迭代器中遍歷時會將訪問的元素移動連結串列到尾部,發生了修改操作。

我們看下LinkedHashMap的get方法:

public V get(Object key) {
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null)
        return null;
    e.recordAccess(this);
    return e.value;
}
void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    if (lm.accessOrder) {
        lm.modCount++;
        remove();
        addBefore(lm.header);
    }
}

可以看到,在get方法中呼叫了e.recordAccess()方法,recordAccess方法中如果accessOrder=true會有修改操作。所以在迭代器中迴圈get時出錯了。

但是,如果把遍歷的程式碼改一下,執行就正常了。

for (Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator();iterator.hasNext();) {
    Map.Entry<String,String> entry = iterator.next();
    System.out.println(entry.getKey() + "->" + entry.getValue());
}

沒報錯是因為沒有對資料結構修改,因為直接呼叫的getKey和getValue方法。

TreeMap——另一種排序的Map

TreeMap實現了SortedMap介面,這意味著它可以對元素進行排序。

但TreeMap與LinkedHashMap的排序不同,它是通過指定的Comparator或Comparable確定。

為了確定Key的排序演算法,可以通過2種方式制定:
1.在TreeMap的建構函式中注入一個Comparator

public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

2.使用實現了Comparable介面的Key。

如果不指定Comparable或Compartor,則在put時會報ClassCastException。

Exception in thread "main" java.lang.ClassCastException: com.tommy.core.test.Test$Student cannot be cast to java.lang.Comparable
    at java.util.TreeMap.compare(TreeMap.java:1188)
    at java.util.TreeMap.put(TreeMap.java:531)
    at com.tommy.core.test.Test.main(Test.java:62)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

因為建構函式沒有傳入Comparator,TreeMap認為你的Key(即這裡的Student)應該實現了Comparable介面,所以會做一個轉換,就出錯了。

示例程式碼:

class Student implements Comparable<Student>{
    private int id;
    private String name;
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
    @Override
    public int compareTo(Student o) {
        if (o == null || this.id > o.id
                ) {
            return 1;
        }
        if (this.id == o.id) {
            return 0;
        }
        return -1;
    }
}

TreeMap<Student,Student> map = new TreeMap<>();
Student s1 = new Student(1003,"張三");
Student s2 = new Student(1002,"李四");
Student s3 = new Student(1001,"王五");
map.put(s1,s1);
map.put(s2,s2);
map.put(s3,s3);
for(Iterator<Student> iterator = map.keySet().iterator();iterator.hasNext();) {
    Student student = iterator.next();
    System.out.println(student.id+":"+student.name);
}

或者TreeMap的初始化指定Comparator:

TreeMap<Student, Student> map = new TreeMap<>(new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        Student s1 = (Student) o1;
        Student s2 = (Student) o2;
        boolean f1 = s1 == null && s2 == null;
        return (f1 || s1.id == s2.id) ? 0 : (s1.id > s2.id ? 1 : -1);
    }
});

輸出:

1001:王五
1002:李四
1003:張三

此外,TreeMap還提供了一系列有用的方法,用於獲取大於,小於,以及2個Key直接的子Map的功能。

如果需要使用Map,並且需要實現排序的功能,建議使用TreeMap。