本文最后更新于:2024年4月16日 下午
一、Map接口简述 public interface Map<K,V>
集合采取键值对(key-value)方式 存储数据,其中key必须唯一;value可以不唯一且value对象类型也可以是Map ,类似数组中的元素还可以是数组。例如:学生教务系统中,学号和学生信息就是一个key-value映射,学号是唯一的,信息可能就名字,也可能是姓名年龄专业的一个数据结构。存储时是学号和学生信息一对放一起存进去
Map集合和Set集合很像,因为Set集合的底层就是使用了Map。
Map集合是键值对形式存储值的,所以遍历Map集合无非就是获取键和值,根据实际需求,进行获取键和值。
Map接口提供三种collection试图,允许以键集、值集或键-值集映射关系集的形式查看某个映射的内容。映射顺序 定义为迭代器在映射的 collection 视图上返回其元素的顺序。某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类。
注:
将可变对象用作映射键时必须格外小心。当对象是映射中某个键时,如果以影响 equals 比较的方式更改了对象的值,则映射的行为将是不确定的。此项禁止的一种特殊情况是不允许某个映射将自身作为一个键包含。虽然允许某个映射将自身作为值包含,但请格外小心:在这样的映射上 equals 和 hashCode 方法的定义将不再是明确的。
所有通用的映射实现类应该提供两个“标准的”构造方法:
一个 void(无参数)构造方法,用于创建空映射;
一个是带有单个 Map 类型参数的构造方法,用于创建一个与其参数具有相同键-值映射关系的新映射。
实际上,后一个构造方法允许用户复制任意映射,生成所需类的一个等价映射。尽管无法强制执行此建议(因为接口不能包含构造方法),但是 JDK 中所有通用的映射实现都遵从它。
二、Map实现类 Map接口的常用方法及其源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public interface Map <K, V>{ int size () ; boolean isEmpty () ; boolean containsKey (Object key) ; boolean containsValue (Object value) V get (Object key) ; V put (K key, V value) ; V remove (Object key) ; void putAll (Map<? extends K, ? extends V> m) ; void clear () ; Set<K> KeySet () ; Collection<V> values () ; Set<Map, Entry<K, V>> entrySet () ; interface Entry <K, V> { K getKey () ; V getValue () ; V setValue (V value) ; boolean equals (Object o) ; int hashCode () ; } boolean equals (Object o) ; int hashCode () ; }
方法描述:
方法名
描述
clear()
从此映射中移除所有映射关系(可选操作)。
entrySet()
返回此映射中包含的映射关系的 Set 视图。
get(Object key)
返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
isEmpty()
如果此映射未包含键-值映射关系,则返回 true。
keySet()
返回此映射中包含的键的 Set 视图。
put(K key, V value)
将指定的值与此映射中的指定键关联(可选操作)。
remove(Object key
如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。
equals(Object o)
比较指定的对象与此映射是否相等。
hashCode()
返回此映射的哈希码值。
Map接口的具体实现类:
实现类
描述
HashMap
基于hash表的实现(不同步)
TreeMap
基于红黑树的实现(排序,不同步)
LinkedHashMap
有序的hash表
ConcurrentHashMap
同步的hash表
Hashtable
同步的hash(被ConcurrentHashMap取代)
三、Map的两种取值方式 Map中没有直接取出元素的方法,而是要先转成Set集合,再通过迭代获取元素。取值方式:KeySet、entrySet
1 KeySet 将Map集合中所有的键存入到Set集合。因为Set集合具备迭代器,所以可以通过迭代方法取出所有的键,再根据get()方法,获取每一个键对应的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 interface Map { public static interface Entry { public abstract Object getKey () ; public abstract Object getValue () ; } } class HashMap implements Map { class Hahs implements Map .Entry{ public Object getKey () {}; public Object getValue () {}; } }
2 entrySet 先获取map中的键值关系封装成一个个的entry对象, 存储到一个Set集合中,再迭代这个Set集合, 根据entry获取对应的key和value。
四、HashMap HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在。在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置,我们总是可以通过key快速地存、取value。下面就来分析HashMap的存取。
1 定义 HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作
1 2 3 4 public class HashMap <K,V> extends AbstractMap <K,V> implements Map <K,V>, Cloneable, Serializable { }
2 构造函数 HashMap提供了三个构造函数:
HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。
在这里提到了两个参数:初始容量,加载因子。其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。
构造函数的源码如下(JDK1.8):
1 2 3 4 5 6 7 8 9 10 11 12 public HashMap (int initialCapacity, float loadFactor) { if (initialCapacity < 0 ) throw new IllegalArgumentException ("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException ("Illegal load factor: " + loadFactor); this .loadFactor = loadFactor; this .threshold = tableSizeFor(initialCapacity); }
3 数据结构 HashMap是一个散列表结构,是基于哈希表的Map接口的非同步实现,提供了所有可选的映射操作,并允许使用null作为键值对;但是它不能保证映射顺序。
位桶 Node类型的数组,也成为Hash桶
1 transient Node<K,V>[] table;
Node对象 Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 () { return key; } public final V getValue () { return value; } public final String toString () { return key + "=" + value; } public final int hashCode () { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue (V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals (Object o) { } }static class Entry <K,V> extends HashMap .Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super (hash, key, value, next); } }
红黑树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static final class TreeNode <k,v> extends LinkedHashMap .Entry<k,v> { TreeNode<k,v> parent; TreeNode<k,v> left; TreeNode<k,v> right; TreeNode<k,v> prev; boolean red; TreeNode(int hash, K key, V val, Node<k,v> next) { super (hash, key, val, next); } final TreeNode<k,v> root () { for (TreeNode<k,v> r = this , p;;) { if ((p = r.parent) == null ) return r; r = p; } } }
4 工作原理 HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node 。
hash方法 1 2 3 4 5 6 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
存储实现:put(key,vlaue) 以JDK1.8为例,描述put的基本过程:
①判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
put方法的源码如下,简单分析一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 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 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
基本流程可以理解为如下图:(来源:https://blog.csdn.net/alashanMt/article/details/107684187 )
读取实现:get(key) 通过get方法来获取,而get方法就是通过getNode来取得元素的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public V get (Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } 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 && ((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 ; }
resize方法扩容
在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
例如负载因子为0.75,map填满了75%的bucket时,就会创建原来hashmap两倍大小的bucket,并将原来的对象放入新的bucket数组中,这个过程叫作rehashing。
每次扩展的时候,都是扩展2倍;
扩展后Node对象的位置要么在原位置,要么移动到<原下标+OldCap>位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; if (oldCap > 0 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0 ) { float ft = (float )newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float )MAXIMUM_CAPACITY ? (int )ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab; if (oldTab != null ) { for (int j = 0 ; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) newTab[e.hash & (newCap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , newTab, j, oldCap); else { Node<K,V> loHead = null , loTail = null ; Node<K,V> hiHead = null , hiTail = null ; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0 ) { if (loTail == null ) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null ) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null ); if (loTail != null ) { loTail.next = null ; newTab[j] = loHead; } if (hiTail != null ) { hiTail.next = null ; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
【参考】