Map
类继承框架
四者比较
HashMap
继承结构
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
主要属性
//默认初始化容量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //数组最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认加载因子 //与数组扩容有关,当数组已有元素 > 数组容量×加载因子 时,进行扩容操作 static final float DEFAULT_LOAD_FACTOR = 0.75f; //链表转红黑树的阈值 static final int TREEIFY_THRESHOLD = 8; //红黑树转链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; //可以进行链表转红黑树的最小数组容量 static final int MIN_TREEIFY_CAPACITY = 64; //数组 transient Node<K,V>[] table; //节点 transient Set<Map.Entry<K,V>> entrySet; //元素个数 transient int size; //结合操作次数,quick-fast机制用到 transient int modCount; //扩容的阈值,比如数组大小为16,加载因子为0.75,那么当数组中的元素个数达到16*0.75时,就需要进行扩容 //用来存初始化时的容量,因为哈希表不是new对象调用构造函数的时候创建,而是在存储第一个元素是创建 int threshold;, //加载因子 final float loadFactor;
构造函数
//无参构造 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //指定容量,调用HashMap(int initialCapacity, float loadFactor)进行构造 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //指定加载因子和数组容量 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); } //使用与指定Map相同的映射构造一个新的HashMap 。 HashMap是使用默认负载因子 (0.75) 创建的,初始容量足以在指定的Map 中保存映射。 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
hash值计算
-
先讲个知识点
-
h >>> 16 ,就是将h无符号右移16位
-
>>
有符号右移,在移动时,如果是正数高位补0,负数高位补1 -
>>>
无符号右移,在移动时,高位始终补0 -
那么为什么采取无符号右移呢?这就和它操作的意义有关了
-
-
过程详述
-
首先对key进行null指针判断,为null则为0,不为空则进行计算
-
先获取key值的哈希码值,调用实际类型重写的hashCode函数
-
获取到后将哈希码值右移16位,再与原哈希码值进行异或操作,返回操作后的值
-
进行异或的作用就是让哈希码的高位参与后续的计算,使得元素的存储分布更加散列
-
右移16位的作用就是绝大多数情况下数组的length一般都小于2^16即小于65536
-
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
put
//onlyIfAbsent – 如果为真,则不更改现有值 //evict – 如果为 false,则表处于创建模式 //返回:以前的值,如果没有,则为 null 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) { //tab:存储数组 //p:对应存储位置的链表头节点 //n:数组length //i:数组存储下标 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; }
扩容
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; // double threshold } //旧容量小于或等于0,扩容阈值大于0,意思就是将哈希表的初始化大小存在threshold变量中 else if (oldThr > 0) newCap = oldThr; //旧容量小于或等于0,扩容阈值等于0,意思就是使用默认的大小来初始化哈希表 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; //初始化哈希表数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { //遍历处理老的哈希表 //因为是二倍扩容,所以原来元素的下标就只有两种情况:不变/下标*2,不变的跟低位指针,*2的跟高位指针 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 { // preserve order //高位指针和低位指针 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; }
JDK7和8的不同
不同点 | JDK 7 | JDK 8 |
---|---|---|
hash值计算 | 4次位运算+5次异或 | 1次位运算+1次异或 |
插入 | 头插 | 尾插 |
扩容时机 | 先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容 | 是先插入再扩容的,优点其实是因为为了减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容 |
数据结构 | 数组+链表 | 数组+链表/红黑树 |
存储顺序 | 反序 | 正序 |
LinkedHashMap
-
此实现与HashMap的不同在于,维护了一个双向链表,将元素的插入顺序做了一个记录
-
具体实现是通过对节点结构的调整来实现
//节点结构 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); } } //双向链表的头部 transient LinkedHashMap.Entry<K,V> head; //双向链表的尾部 transient LinkedHashMap.Entry<K,V> tail;
HashTable
-
底层数据结构还是哈希表(数组+链表)
-
安全性是通过对方法加synchronized来保证
ConcurrentHashMap
继承结构
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable
主要属性
//存储数组 transient volatile Node<K,V>[] table; //扩容时才使用的数组 private transient volatile Node<K,V>[] nextTable; //基本计数器,主要在没有竞争时使用,也可作为表初始化竞争期间的后备 private transient volatile long baseCount; //表初始化和调整大小控制。 如果为负,则表正在初始化或调整大小。初始化后,保存下一个要调整表格大小的数值 private transient volatile int sizeCtl; //调整大小和/或创建CounterCell时使用的自旋锁(通过CAS锁定) private transient volatile int cellsBusy;
添加
-
在数组对应位置没有节点的情况下,使用CAS来添加元素
-
有节点的情况下会用synchronized声明的代码块锁住,然后进行一个添加操作
扩容
-
新数组的容量由单线程进行设置
-
元素的移动由多线程完成
-
通过给格子设置标志,加锁来保证线程安全性