Java 中 HashMap 的实现解析
2017-03-19 16:31
405 查看
转载请注明出处:http://blog.csdn.net/hjf_huangjinfu/article/details/63684337
HashMap 作为一个散列表,基于 散列 的方式,实现一个 Map。下面看一下它在具体实现方面的一些点。备注:a^b 为 a的b次方。
1、基本实现方法
HashMap 内部把每一对键值对封装成Node,然后以Node数组为主,Node链表为辅 来存放数据。根据每个元素的 hashcode 来计算出元素在数组中相应的存放位置(索引)。所以理论上可以使插入和查找的时间复杂度降低为 O(1)。2、容量分配策略
HashMap 内部的数组容量大小设定有一定的特点,那就是,都是 2^n (n >= 4 && n<= 30),数组容量的最小值,是 2^4,也就是 16。/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
3、Hash 策略(索引计算策略)
Java期望每个对象都可以合理的重写自己的 hashCode 方法,然后用一个 int 类型的值来表示 hash 值。hash策略就是用来根据每个元素的 hashcode 来计算出元素在数组中相应的存放位置(索引),我们可以知道,int 的值范围是 -2^31 ~ 2^31-1,而 HashMap 内部数组的索引范围是 0 ~ size-1(size 是数组大小)。转为2进制看起来会比较方便:
int 的范围:0x10000000 00000000 00000000 00000000 ~ 0x01111111 11111111 11111111 11111111
索引的范围:0 ~ 0x0...01...1,0和1的个数取决于数组的 size 大小。
HashMap 重新处理了每个对象的 hashCode,把高位2个字节保持不变,低位2个字节替换为高位两字节与低位2字节的异或后的值:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
HashMap 计算元素在数组中的索引的方法是 直接取 处理后的 hash 的低 n 位(数组大小为 2^n)。
p = tab[i = (n - 1) & hash])
为什么要对原始的 hash 做处理?
如果不这样做处理的话,那么假如有一个 hash 集合,他们的值低位2个字节不变,只变化高位2个字节的话(比如 0x1234ffff 和 0x5678ffff),并且集合大小 size < 2^16 ,这个集合中所有 hash 计算出来的地址的值就会一样,hash 冲突概率 100%,这样就等于没有使用 hash 的优点。处理是为了让高位2个字节也参与元素索引的计算,降低hash冲突的概率。4、Hash冲突解决策略
只要是使用 hash,不可避免的就会导致 hash 冲突(不同的值经过hash函数计算后,输出的值相同),HashMap 中采用了 拉链法(具有相同 hash 的元素会链接到上一个元素的结尾) 来解决 hash 冲突,。但是 HashMap 内部对此作了优化,当某一个 hash 冲突的元素数量达到 8 个 后,HashMap 内部会把这 8 个元素 的普通单向链表转化为一颗红黑树(插入、查找、删除的时间复杂度为 log2N )。这样在局部可以提升查找插入的性能。
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8;
当冲突元素数量逐渐降低,降为 6 的时候,HashMap 内部就会把 红黑树 转化为普通的单向链表。
/** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6;
5、扩容时候的元素位置调整
当容量不够用的时候,HashMap 内部会自动进行扩容操作,扩容规模为当前容量的 2 倍,也就是 size = size << 1; ,那么问题来了,并不是简单的创建一个新数组,然后把旧数据复制到新数组的相应位置就行了。因为这个位置是经过计算出来的,而数组的容量也是参与运算的元素之一,设想这样两个元素:元素1的 hash = 0b01101010 = 106;
元素2的 hash = 0b00101010 = 42;
数组容量 size= 0b01000000 = 64;
元素1的 index = 0b01101010 & 0b00111111 = 42;
元素2的 index = 0b00101010 & 0b00111111 = 42;
此时,元素1 和 元素 2 的索引,计算出来都是 index = 0b00101010 = 42;
但是当数组扩容后:
元素1的hash = 0b01101010 = 106;
元素2的hash = 0b00101010 = 42;
数组容量 size= 0b01000000 = 64;
元素1的 index = 0b01101010 & 0b01111111 = 106;
元素2的 index = 0b00101010 & 0b01111111 = 42;
所以就需要重组数据,重新调整数据的存储位置以及方式。但是每个数据只有2种情况,要么索引不变,要么索引就会向后偏移旧容量的大小(106 - 42 = 64)。
6、如何正确的使用HashMap
一个正确的使用方式,可以最大限度的提升 HashMap 的性能。既然是基于 hash机制,那么 hash算法的性能就尤为重要,这里内部的索引计算方式不谈,我们把目光转移到元素的 hashCode 方法上面,毕竟大多数情况,对象的 hashCode 都是由开发者来覆写,那么如何高质量的覆写呢。先来看一下 hashCode 的方法说明:当两个对象 equals 的时候(逻辑上相等),hashCode 方法必须返回相同的值。但是,两个不 equals 的对象的 hashCode方法,并不是一定要返回两个不相同的值。但是我们要知道,这种情况下,返回不同的值,会提升基于 hash 机制的算法的效率。
假如某个开发者贪图方便,hashCode 随笔一写,或者所有对象都返回相同的 hash 值,那么在使用 HashMap 的时候,你可能会发现,效率很低。效率已经从 O(1) 变为 O(log2n)。
因为,基于上述的分析结果,一个工作中的 HashMap,它的数据存储大概是这样的:
7、官方推荐的 hashCode 覆写方式
@Override public int hashCode() { // Start with a non-zero constant. int result = 17; // Include a hash for each field. result = 31 * result + (booleanField ? 1 : 0); result = 31 * result + byteField; result = 31 * result + charField; result = 31 * result + shortField; result = 31 * result + intField; result = 31 * result + (int) (longField ^ (longField >>> 32)); result = 31 * result + Float.floatToIntBits(floatField); long doubleFieldBits = Double.doubleToLongBits(doubleField); result = 31 * result + (int) (doubleFieldBits ^ (doubleFieldBits >>> 32)); result = 31 * result + Arrays.hashCode(arrayField); result = 31 * result + referenceField.hashCode(); result = 31 * result + (nullableReferenceField == null ? 0 : nullableReferenceField.hashCode()); return result; }
8、Iterator 遍历顺序
HashMap 中 Iterator 的遍历顺序是乱序的,也就是不能保证按 插入顺序 输出集合中的元素。示例图如下:
这里有一个 局部乱序 要提一下:就是说,在整体上遍历顺序是从元素数组的 0 索引开始,一直到数组结束。遇到单链表的情况时,也是顺着此单链表顺序遍历的,但是当 hash 冲突比较严重,导致有红黑树的时候,此时遍历顺序,并不是按照树的结构遍历(前序、中序、后序、层序),而是乱序的,因为在把单链转换为红黑树的时候,除了把元素组织成了树结构,他还用单链表组织了数据的关系(图中没有画出)。其实是按照那个单链表的顺序来遍历的。
单链转红黑树:
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; //使用单链表来组织元素 tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
遍历:
final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null){ throw new NoSuchElementException(); } //如果在tab中遇到空元素,跳过,找到下一个非空元素,否则,按单链表顺序遍历 if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; }
相关文章推荐
- 深入解析java HashMap实现原理
- Java集合-HashMap源码实现深入解析
- 数据结构之应用 "栈(Stack)" 实现: 解析算术表达式及计算求值 (C#/Java) (转载)
- java实现读取hashmap的方法
- 设计模式解析和实现(C++, java)之二十--状态(state)模式
- ASP中实现java中hashMap的类
- 哈希算法-----JAVA 源码中实现的HashMap学习总结
- 数据结构之应用 "栈(Stack)" 实现: 解析算术表达式及计算求值 (C#/Java)
- Java HashMap实现详解
- 数据结构之应用 "栈(Stack)" 实现: 解析算术表达式及计算求值 (C#/Java)
- 源码解读:java 解析字符串为boolean四种实现方法的细节
- Java HashMap实现详解(转)
- 数据结构之应用 "栈(Stack)" 实现: 解析算术表达式及计算求值 (C#/Java)
- java.beans包里面的两个类简单地实现XML解析
- 用java中的HashMap实现队列
- 设计模式解析和实现(C++, java)之十-singleton模式
- 数据结构习作之应用 "栈(Stack)" 实现: 解析算术表达式及计算求值 (C#/Java) (技术含量少许)
- 如何用Java 实现 Excel 表达式的解析(摘自:http://topic.csdn.net/t/20030408/17/1634982.html#)
- java实现对url解析
- java轻松实现购物车(HashMap技术实现购物车)