Java集合(四):Map映射
集是一個集合,它可以快速的查找現有的元素。但是,要查看一個元素,需要有要查找元素的精確副本。這不是一個非常通用的查找方式。通常,我們知道某些鍵的信息,并想要查找與之對應的元素。映射表(map)就是為此設計的。映射表用來存儲鍵值對。如果提供了鍵,就可以查找對應的值。例如,有一張關于員工信息的記錄表,鍵為員工ID,值為Employee。
Java類庫為映射表提供了兩個通用的實現:HashMap和TreeMap,這兩個類都實現了Map接口。
散列映射表對鍵進行散列,樹映射表用鍵的整體順序對元素進行排序,并將其組織成搜索樹。散列或比較函數只能作用于鍵。與鍵關聯的值不能進行散列或比較。
應該選擇散列映射表還是樹映射表呢?散列稍微快一些,如果不需要按照排列順序訪問鍵,就最好選擇散列。
本文主要介紹HashMap類的底層實現,并簡單介紹TreeMap但不做過多分析,因為TreeMap使用紅黑樹實現的,比較復雜,等以后再看。
1 Map接口
Map接口提供了一些映射表的基本操作,下面是這些方法的總結:
(1)查詢操作
int size(); boolean isEmpty(); boolean containsKey(Object); boolean containsValue(Object); V get(Object);這些方法的含義都很明確。需要注意的是,containsKey方法、containsValue方法和get方法的參數類型都是Object。(2)修改方法
V put(K,V); V remove(Object); void putAll(Map<? extends K,? extends V>); void clear();put方法用于添加一個鍵值對,如果鍵已經存在就更新值并返回舊值。remove方法刪除給定鍵的鍵值對并返回值。putAll方法將一個Map中的所有鍵值對添加到映射表中。clear方法刪除所有元素。(3)視圖方法
Set<K> keySet(); Collection<V> values(); Set<Map.Entry<K,V>> entrySet();這三個方法返回三個視圖:鍵集、值集合(不是集)和鍵值對集。對于視圖會在后續的文章中作介紹。在Map接口中還定義了一個子接口:Entry,用來操作鍵值對。
這個接口主要有一下幾個方法:
K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode();含義也比較明確。2 散列映射表:HashMap
散列映射表主要用到散列技術,可以快速對一個元素進行查找。HashMap類中的主要域如下:
transient Node<K,V>[] table; transient int size; int threshold; final float loadFactor;其中使用table來存儲元素,size表示映射表中鍵值對的個數,threshold是一個域值,當元素個數超過這個域值后,就會自動擴展映射表的大小。而loadFactor是一個加載因子,表示threshold與table長度的比值。可以看到,table是一個數組,數組中存儲Node<K,V>類型的值。Node<K,V>表示一個鍵值對,定義如下:
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey();public final V getValue();public final String toString();public final int hashCode();public final V setValue(V newValue);public final boolean equals(Object o); }是一個靜態內部類,這表示一個鍵值對,可見HashMap將鍵值對作為一個整體來操作。在Node中,有存儲鍵的key,存儲值的value,存儲散列值的hash,還有一個next引用,可見這是一個鏈表。
既然有散列值hash,那么這個值是如何計算的呢?方法如下:
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }是一個純粹的數學方式。有了散列值,HashMap又是如何散列的呢?
HashMap使用hash值確定一個鍵值對在table中的位置,具體方法是,用hash%table.length,結果就是在table中的下標。如果有多個hash在table中的同一個位置,那么就構成一個鏈表。存儲方式大致是這樣的:
在HashMap中,table的默認大小是16,以后每次擴大容量都會是原來的二倍,因此,table的大小一直是2的冪。由于這點,HashMap在計算一個hash的位置的時候,使用了非常巧妙的方法:
int n=table.length; int index=hash&(n-1);這就相當于計算hash%table.length。了解了鍵值對的表示方式和HashMap的存儲方式之后,就要對鍵值對進行操作了。常見的操作有查找、插入和刪除。接下來就介紹這些操作:
(1)查找
HashMap中,對于查找操作,定義了一個私有方法getNode。這個方法有兩個參數:hash和key,根據哈希值和鍵來找鍵值對。定義如下:
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null; }首先根據hash定位鍵值對在table中的位置,找到之后首先檢查第一個元素,如果符合就返回,如果不符合就遍歷這個鏈表,知道找到符合的鍵值對。如果沒有找到,說明沒有這個鍵值對,返回null。這個方法是查找操作的基本方法,HashMap中的查找方法比如containsKey等都是調用這個方法完成操作的。
(2)添加鍵值對
HashMap中定義了一個基本方法putVal,這個方法將給定的鍵和值加入映射表中,定義如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//如果表為空,即里面沒有元素,則使用resize方法創建一個表if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//找到給定的鍵值對對應的位置,如果對應位置還沒有元素,則創建一個Node作為鏈表的頭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 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null; }添加操作和查找操作有點類似,首先定位待插入的鍵值對在table中的位置,如果里面沒有元素,直接插入即可;如果里面已經有元素,即發生了沖突,就將這個元素加入到這個鏈表中。put方法就是調用這個方法的。
(3)刪除鍵值對
有加入操作就有刪除操作。基本方法是removeNode:
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)tab[index] = node.next;elsep.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null; }首先也是定位到節點的位置,matchValue是是否匹配值,如果為true,就是說只有值也匹配的時候才刪除這個鍵值對。HashMap中的remove方法就是調用這個方法。(4)容量擴展
當當前元素個數size等于threshold時,即使沒有達到table的容量,也需要對table進行擴展。HashMap中的resize方法完成這個操作。
這個方法比較復雜,方法分為兩部分,第一個部分就是確定新映射表的大小,考慮的主要問題是數值溢出。因為默認的大小是16,每次擴容都會是原來的2倍,很容易溢出。當發生這種情況時,就將threshold值置為Integer.MAX_VALUE。
第二個部分就是將原來映射表里的內容移到新的映射表中。這只需要兩層循環就好。第一層循環是在table數組上,第二層是每個table元素也是一個鏈表,需要循環一次。然后把每個鍵值對放在新的映射表中的合適位置即可。
以上就是一些基本的操作,HashMap的修刪改查方法都是基于這些方法實現的。
接下來就是HashMap的視圖(view)操作。
(1)鍵集:keySet
方法keySet可以返回一個由鍵構成的集,注意KeySet既不是HashSet,也不是TreeSet,而是擴展了AbstractSet抽象類的一個內部類:
final class KeySet extends AbstractSet<K> {public final int size() { return size; }public final void clear() { HashMap.this.clear(); }public final Iterator<K> iterator() { return new KeyIterator(); }public final boolean contains(Object o) { return containsKey(o); }public final boolean remove(Object key) {return removeNode(hash(key), key, null, false, true) != null;}public final Spliterator<K> spliterator() {return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);}public final void forEach(Consumer<? super K> action) {Node<K,V>[] tab;if (action == null)throw new NullPointerException();if (size > 0 && (tab = table) != null) {int mc = modCount;for (int i = 0; i < tab.length; ++i) {for (Node<K,V> e = tab[i]; e != null; e = e.next)action.accept(e.key);}if (modCount != mc)throw new ConcurrentModificationException();}} }這個類實現了Set接口,也是一個Collection,因此可以與使用任何集合一樣使用keySet。比如,可以枚舉映射表中的所有鍵:
Set<String> keys=map.keySet(); for(String key:keys) {do something with key }(2)值集合:valuesvalues方法返回一個由值構成的集合,注意不是集,因為HashMap僅要求鍵唯一,不需要值唯一。返回的這個集合是擴展了AbstractCollection類的一個內部類:
final class Values extends AbstractCollection<V>(3)鍵值對集合:entrySet這個方法返回由所有鍵值對構成的集合。這個方法返回的集是擴展了AbstractSet類的內部類:
final class EntrySet extends AbstractSet<Map.Entry<K,V>>這樣,就可以同時查看鍵和值了,以避免對值進行查找: for(Map.Entry<String,Employee> entry:staff.entrySet()) {String key=entry.getKey();Employee value=entry.getValue();dosomething with key,value }下面的代碼演示了映射表的操作過程。首先將鍵值對添加到映射表中。然后,從映射表中刪除一個鍵,同時與之對應的值也別刪除了。接下來,修改與某一個鍵對應的值,并調用get方法獲得這個值。最后,對元素進行迭代: import java.util.*; public class MapTest {public static void main(String[] args){Map<String, Employee> staff = new HashMap<>();staff.put("144-25-5464", new Employee("Amy Lee"));staff.put("567-24-2546", new Employee("Harry Hacker"));staff.put("157-62-7935", new Employee("Gary Cooper"));staff.put("456-62-5527", new Employee("Francesca Cruz"));// print all entriesSystem.out.println(staff);// remove an entrystaff.remove("567-24-2546");// replace an entrystaff.put("456-62-5527", new Employee("Francesca Miller"));// look up a valueSystem.out.println(staff.get("157-62-7935"));// iterate through all entriesfor (Map.Entry<String, Employee> entry : staff.entrySet()){String key = entry.getKey();Employee value = entry.getValue();System.out.println("key=" + key + ", value=" + value);}} }結果如下:
當創建 HashMap 時,有一個默認的負載因子(load factor),其默認值為 0.75,這是時間和空間成本上一種折衷:增大負載因子可以減少 Hash 表(就是那個 Entry 數組)所占用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put() 方法都要用到查詢);減小負載因子會提高數據查詢的性能,但會增加 Hash 表所占用的內存空間。
掌握了上面知識之后,我們可以在創建 HashMap 時根據實際需要適當地調整 load factor 的值;如果程序比較關心空間開銷、內存比較緊張,可以適當地增加負載因子;如果程序比較關心時間開銷,內存比較寬裕則可以適當的減少負載因子。通常情況下,程序員無需改變負載因子的值。
如果開始就知道 HashMap 會保存多個 key-value 對,可以在創建時就使用較大的初始化容量,如果 HashMap 中 Entry 的數量一直不會超過極限容量(capacity * load factor),HashMap 就無需調用 resize() 方法重新分配 table 數組,從而保證較好的性能。當然,開始就將初始容量設置太高可能會浪費空間(系統需要創建一個長度為 capacity 的 Entry 數組),因此創建 HashMap 時初始化容量設置也需要小心對待。?
3 樹映射表:TreeMap
TreeMap用鍵的整體順序對元素進行排序,底層使用紅黑樹實現。迭代時,會按照順序迭代。
下面的代碼演示了TreeMap的使用。這里使用的是默認的比較器:
public class MapTest {public static void main(String[] args){Map<String, Employee> staff = new TreeMap<>();staff.put("144-25-5464", new Employee("Amy Lee",9000));staff.put("567-24-2546", new Employee("Harry Hacker",5000));staff.put("157-62-7935", new Employee("Gary Cooper",7500));staff.put("456-62-5527", new Employee("Francesca Cruz",8000));for(Map.Entry<String,Employee> entry:staff.entrySet()){String key = entry.getKey();Employee value = entry.getValue();System.out.println("key=" + key + ", value=" + value);}} }結果如下:
TreeMap在構造時還可以指定一個比較器,根據比較器對鍵進行排序:
public class MapTest {public static void main(String[] args){Map<String, Employee> staff = new TreeMap<>(new Comparator<String>() {@Overridepublic int compare(String o1, String o2) {return o2.compareTo(o1);}});staff.put("144-25-5464", new Employee("Amy Lee",9000));staff.put("567-24-2546", new Employee("Harry Hacker",5000));staff.put("157-62-7935", new Employee("Gary Cooper",7500));staff.put("456-62-5527", new Employee("Francesca Cruz",8000));for(Map.Entry<String,Employee> entry:staff.entrySet()){String key = entry.getKey();Employee value = entry.getValue();System.out.println("key=" + key + ", value=" + value);}} }這里構造一個比較器,使得按照鍵反向排列。結果如下:
注意,TreeMap只能對鍵進行排序,不能對與鍵關聯的值進行排序。
總結
以上是生活随笔為你收集整理的Java集合(四):Map映射的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第五人格自定义模式怎么添加人机
- 下一篇: Java集合(五):Set集