Java 学习系列:HashMap 的实现原理
2016-03-30 18:36
357 查看
1 . HashMap 概述:
Hash 表实现了Map接口,可以执行Map所含的所有方法,并且允许Null key和Null值;(HashMap和Hashtable是大部分是相同的,除了前者是非同步的且允许Null对象!)HashMap是无序的,它并不保证元素的顺序随着时间变化而不变。
HashMap对外提供表现一致的put和get方法,提供一个hash函数对容器里面的元素进行分散到固定块存储,并且随着时间的推进,内部块的大小会成比例的增加。因此不需要在初始化时把容器大小设置的过大;
每个HashMap的实例包含两个参数会影响到它的表现,分别是initial capacity和load factor;这个capacity是Hash表内部的容器空间大小,而load factor是当前容器允许的最大可承载元素大小的指数;当容器内部元素个数接近达到load factor要求及current capacity大小时,这个Hash 表就会重新构建分布(内部数据结构会重构),它的容器大概会扩增到之前大小的两倍左右;
通常默认的load factor是 (.75),这时会使时间与空间消耗的效率达到最优。比这个值高可以节约一点空间消耗,但会增加Map相关所有操作的查找时间(包括put,get,等)。当设置Hash表的初始大小时,应该把所预计的实体大小和load factor的大小一起计算到其中,以减少rehash的次数;如果Hash表的初始大小大于由load factor分散的预计实体集合的大小,那就永远不会发生rehash操作;
当我们要用一个Hash表存放大量的映射对象时,可以创建一个初始化大小很大的对象,那样会提高put的效率;请注意,如果操作很多对于key的hashCode一样的映射对象时,无疑对于任何hash table都会降低效率,因此为了降低这种影响,当key对象实现了Comparable时,可以用对应class的比较方式对这种key的顺序进行区分;
(内部首先是一个根据hash key定位的数组,然后数组中的每一个元素都应该是一个链表)
对于从Hash表得到的iterators是立即失败的,如果我们在获取到一个集合对象的迭代器后,对集合的结构进行修改,然后再执行iterators的相关方法,那iterator就会抛出一个异常ConcurrentModificationException。但是通常来讲,对于任何不并发的数据结构,我们都不能保证这种快速失败是立即生效,所以依据此规则编写我们的应用是错误的,迭代器可以快速的给我们传递集合已被修改的信息。这一点仅可以被我们用来查找bug。
2 . 相关内容 (下面的全部依据jdk1.8,因为java8对HashMap进行了优化)
1) put 函数的实现思路:
对key的hashCode()做hash,然后再计算index;
判断bucket空间大小(超过load factor*current capacity),进行必要的resize,然后再查找,设置新值,返回旧值.
如果根据hash找到碰撞节点,直接创建一个新节点(应该是一个新的链表并且要放入的值则是新链表中第一个元素
如果刚好碰撞并且并且key相同,表明找到需要进行旧值替换;
如果碰撞了,判断碰撞节点的类型,如果是树(红黑树),则调用对应的putTreeVal方法,如果不是那就以链表的方式对待依次往下一个节点查找直到找到为止 ;
2) put函数具体实现代码如下:
3) get 函数的实现思路
对key的hashCode()做hash,然后再计算index;
如果根据hash找到碰撞节点,判断key的值,如果相同直接命中返回;
如果key不相等,则去它的next节点去找,判断next节点的类型;
若为树(红黑树),则调用对应的getTreeNode方法,效率为O(logn);
若为链表,则在链表中通过key.equals(k)查找,O(n);
4) get函数具体实现代码如下:
5) hash函数实现:
jdk1.8之前的Hash函数实现:
6) resize函数实现:
3 . 其他
关于Java8的优化, 并不是当出现相同Hash Key时的多个元素就立即使用的树结构存放同Hashkey的节点, 大概的逻辑是, 最开始还是用的链表数据结构(HashMap.Node), 而当链表大小超过7时, 才替换为红黑树(HashMap.TreeNode);
注意HashMap是非同步的,所以当有多个线程操作连表时并且有一个线程在修改它的结构时,我们要自己保证同步线程安全,可以通过它实现:
注意HashMap是非线程安全的, 当在多线程环境下同时访问它时会出几个问题,
<1>. 添加节点的操作与map size加一操作不是原子操作, 在多线程情况下同时进行元素添加会丢失数据,最终导致会出现size的表现值与Map实际持有元素个数不一致的情况发生;
<2>. 当Map由于元素增多而需要扩容时, 生成比原始容器大的新容器时会进行元素转移(transfer)的过程中, 多线程访问可能会造成链表元素闭环, 这样等下一次get操作命中该链表时就可能会产生死循环;
在多线程并发环境下, 应该使用位于java.util.concurrent包下的ConcurrentHashMap集合对象;
Hash 表实现了Map接口,可以执行Map所含的所有方法,并且允许Null key和Null值;(HashMap和Hashtable是大部分是相同的,除了前者是非同步的且允许Null对象!)HashMap是无序的,它并不保证元素的顺序随着时间变化而不变。
HashMap对外提供表现一致的put和get方法,提供一个hash函数对容器里面的元素进行分散到固定块存储,并且随着时间的推进,内部块的大小会成比例的增加。因此不需要在初始化时把容器大小设置的过大;
每个HashMap的实例包含两个参数会影响到它的表现,分别是initial capacity和load factor;这个capacity是Hash表内部的容器空间大小,而load factor是当前容器允许的最大可承载元素大小的指数;当容器内部元素个数接近达到load factor要求及current capacity大小时,这个Hash 表就会重新构建分布(内部数据结构会重构),它的容器大概会扩增到之前大小的两倍左右;
通常默认的load factor是 (.75),这时会使时间与空间消耗的效率达到最优。比这个值高可以节约一点空间消耗,但会增加Map相关所有操作的查找时间(包括put,get,等)。当设置Hash表的初始大小时,应该把所预计的实体大小和load factor的大小一起计算到其中,以减少rehash的次数;如果Hash表的初始大小大于由load factor分散的预计实体集合的大小,那就永远不会发生rehash操作;
当我们要用一个Hash表存放大量的映射对象时,可以创建一个初始化大小很大的对象,那样会提高put的效率;请注意,如果操作很多对于key的hashCode一样的映射对象时,无疑对于任何hash table都会降低效率,因此为了降低这种影响,当key对象实现了Comparable时,可以用对应class的比较方式对这种key的顺序进行区分;
(内部首先是一个根据hash key定位的数组,然后数组中的每一个元素都应该是一个链表)
Map m = Collections.synchronizedMap(new HashMap(...));
对于从Hash表得到的iterators是立即失败的,如果我们在获取到一个集合对象的迭代器后,对集合的结构进行修改,然后再执行iterators的相关方法,那iterator就会抛出一个异常ConcurrentModificationException。但是通常来讲,对于任何不并发的数据结构,我们都不能保证这种快速失败是立即生效,所以依据此规则编写我们的应用是错误的,迭代器可以快速的给我们传递集合已被修改的信息。这一点仅可以被我们用来查找bug。
2 . 相关内容 (下面的全部依据jdk1.8,因为java8对HashMap进行了优化)
1) put 函数的实现思路:
对key的hashCode()做hash,然后再计算index;
判断bucket空间大小(超过load factor*current capacity),进行必要的resize,然后再查找,设置新值,返回旧值.
如果根据hash找到碰撞节点,直接创建一个新节点(应该是一个新的链表并且要放入的值则是新链表中第一个元素
如果刚好碰撞并且并且key相同,表明找到需要进行旧值替换;
如果碰撞了,判断碰撞节点的类型,如果是树(红黑树),则调用对应的putTreeVal方法,如果不是那就以链表的方式对待依次往下一个节点查找直到找到为止 ;
2) put函数具体实现代码如下:
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ 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) // -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; }
3) get 函数的实现思路
对key的hashCode()做hash,然后再计算index;
如果根据hash找到碰撞节点,判断key的值,如果相同直接命中返回;
如果key不相等,则去它的next节点去找,判断next节点的类型;
若为树(红黑树),则调用对应的getTreeNode方法,效率为O(logn);
若为链表,则在链表中通过key.equals(k)查找,O(n);
4) get函数具体实现代码如下:
/** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ 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; }
5) hash函数实现:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//这里采用了,高位不变,低16位与高16位做了一个异或(这是一个顾全大局的做法,综合考虑了速度,作用,质量)。 }
jdk1.8之前的Hash函数实现:
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k);//如果是字符串的key,就使用系统的stringHash32函数计算对应的hash值 } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
6) resize函数实现:
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ 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 } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults 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 { // 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; }
3 . 其他
关于Java8的优化, 并不是当出现相同Hash Key时的多个元素就立即使用的树结构存放同Hashkey的节点, 大概的逻辑是, 最开始还是用的链表数据结构(HashMap.Node), 而当链表大小超过7时, 才替换为红黑树(HashMap.TreeNode);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);
注意HashMap是非同步的,所以当有多个线程操作连表时并且有一个线程在修改它的结构时,我们要自己保证同步线程安全,可以通过它实现:
注意HashMap是非线程安全的, 当在多线程环境下同时访问它时会出几个问题,
<1>. 添加节点的操作与map size加一操作不是原子操作, 在多线程情况下同时进行元素添加会丢失数据,最终导致会出现size的表现值与Map实际持有元素个数不一致的情况发生;
<2>. 当Map由于元素增多而需要扩容时, 生成比原始容器大的新容器时会进行元素转移(transfer)的过程中, 多线程访问可能会造成链表元素闭环, 这样等下一次get操作命中该链表时就可能会产生死循环;
在多线程并发环境下, 应该使用位于java.util.concurrent包下的ConcurrentHashMap集合对象;
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c语言实现hashmap(转载)
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序