在讨论哈希表以前,咱们先大概了解下其余数据结构在新增,查找等基础操做执行性能html
数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);经过给定值进行查找,须要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),固然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提升为O(logn);对于通常的插入删除操做,涉及到数组元素的移动,其平均复杂度也为O(n)java
线性链表:对于链表的新增,删除等操做(在找到指定操做位置后),仅需处理结点间的引用便可,时间复杂度为O(1),而查找操做须要遍历链表逐一进行比对,复杂度为O(n)node
二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操做,平均复杂度均为O(logn)。面试
哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操做,性能十分之高,不考虑哈希冲突的状况下,仅需一次定位便可完成,时间复杂度为O(1),接下来咱们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。数组
咱们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面咱们提到过,在数组中根据下标查找某个元素,一次定位就能够达到,哈希表利用了这种特性,哈希表的主干就是数组。安全
好比咱们要新增或查找某个元素,咱们经过把当前元素的关键字 经过某个函数映射到数组中的某个位置,经过数组下标一次定位就可完成操做。
数据结构
存储位置 = f(关键字)app
其中,这个函数f通常称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,好比咱们要在哈希表中执行插入操做:ide
查找操做同理,先经过哈希函数计算出实际存储地址,而后从数组中对应地址取出便可。函数
哈希冲突
然而万事无完美,若是两个不一样的元素,经过哈希函数得出的实际存储地址相同怎么办?也就是说,当咱们对某个元素进行哈希运算,获得一个存储地址,而后要进行插入的时候,发现已经被其余元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面咱们提到过,哈希函数的设计相当重要,好的哈希函数会尽量地保证 计算简单和散列地址分布均匀,可是,咱们须要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证获得的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap便是采用了链地址法,也就是数组+链表的方式。
1)HashMap是由数组+链表+红黑树构成的,数组就称之为桶了
HashMap 根据键的 hashCode 值存储数据,大多数状况下能够直接定位到它的值,于是具备很快的访问速度,但遍历顺序倒是不肯定的。 HashMap 最多只容许一条记录的键为 null ,容许多条记录的值为 null 。HashMap 非线程安全,即任一时刻能够有多个线程同时写 HashMap,可能会致使数据的不一致。若是须要知足线程安全,能够用 Collections的synchronizedMap 方法使 HashMap 具备线程安全的能力,或者使用ConcurrentHashMap ,或者或者其它。。。。。。
基本指标:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化容量为16,must be a power of two,详见下面 static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量1G static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子 static final int TREEIFY_THRESHOLD = 8;// 从链表变为红黑树的阈值,当链表长度大于等于8时,由链表转换成红黑树 static final int UNTREEIFY_THRESHOLD = 6; // 从红黑树变为链表的阈值 // 当须要将解决 hash 冲突的链表转变为红黑树时,须要判断下此时数组容量,如果因为数组容量过小(小于 MIN_TREEIFY_CAPACITY )致使的 hash 冲突太多,则不进行链表转变为红黑树操做,转为利用 resize() 函数对 hashMap 扩容 // 当某个桶中的链表长度达到8进行链表扭转为红黑树的时候,会检查总桶数是否小于64,若是总桶数小于64也会进行扩容; static final int MIN_TREEIFY_CAPACITY = 64;
成员变量:
//好比说,在初始化时,默认的容量是16,那么table的length就是16,其threshold=容量×负载因子=16×0.75=12,这就表明着,当size大于12时,就会进行扩容(容量会×2,threshold会根据新容量从新计算)的操做! //这样作的目的很明确,就是为了减小哈希冲突!有效元素的个数少于哈希表的总大小时,其产生哈希冲突的可能性必定是小于相等状况的! transient Node<K,V>[] table; // 真正开辟的空间,其length就是真正的容量大小;真正占用空间(用不用是一回事,先占用先);When allocated, length is always a power of two. transient int size; // 真正使用的空间;有效的结点个数;总的键值对的个数; The number of key-value mappings contained in this map. int threshold; // 阈值,大于这个值,扩容;The next size value at which to resize (capacity * load factor);用来记录当前容量下,最适合存放多少键值对(容量*负载因子) final float loadFactor; // 负载因子,默认0.75 transient int modCount; //用于快速失败,因为HashMap非线程安全,在对HashMap进行迭代时,若是期间其余线程的参与致使HashMap的结构发生变化了(好比put,remove等操做),须要抛出异常ConcurrentModificationException transient Set<Map.Entry<K,V>> entrySet; // //由 hashMap 中 Node<K,V> 节点构成的 set
静态工具:
/** * Node<K, V>是一个静态内部类,封装了这个结点的全部信息 */ static class Node<K,V> implements Map.Entry<K,V>{ final int hash; // 相对应的hash值,其方法见下 final K key; V value; Node<K,V> next; // 链表中指向下一处的指针;为了解决哈希冲突,当产生哈希冲突时,next就能够指向一张链表,或者一棵黑树! ...... } /** * 计算其hash值的方法,看不懂 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /** * 这样的设计一切都是为了性能 * 当 table.length 知足2的整数次幂时,如下条件成立: * hash & (table.length - 1) == hash % table.length */ hash & (table.length - 1) /** * 方法返回的值是最接近 initialCapacity 的2的幂,这些位的设计一切都是为了性能 * 若指定初始容量为9,则返回16, * 若指定初始容量为5,则返回8, * @param cap * @return */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
构造方法:
// 赋值阈值以及负载因子初始化 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); // 阈值初始化,最后仍是变成容量的初始化。见:resize() 的 0】、2-1】,这里说白了就只是一层迷惑人的转换罢了 } // 默认负载因子为0.75了 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 最经常使用的hashMap的构造器 * 这里只赋值了负载因子哦 * 阈值以及容量是在put的时候搞进去的,位于 resize()方法的 4】 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
put():
1)若是存在Hash碰撞就会以链表的形式保存,把当前传进来的参数生成一个新的节点保存在链表的尾部(JDK1.7保存在首部)。而若是链表的长度大于8那么就会以红黑树的形式进行保存(位于 3-3-3-1】)。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; // 这里的tab就是指这个table,只不过不用table罢了 Node<K,V> p; //指hashMap里面的table数组下标的node值 p=table[i] (就是指那个槽) int n;//指hashMap里面的table数组长度 int i;//指hashMap里面的table数组下标(就是那个槽的下标) // 1】若是table未初始化或长度为0,则进行初始化(马上扩容)(hashMap的最经常使用的构造函数第一次初始化就是在这里) if ((tab = table) == null || (n = tab.length) == 0) { n = (tab = resize()).length;// 1-1】初始化的时候,调用resize()方法,获得hashmap里面的数组长度。空参构造后的put()方法的阈值以及容量都是在resize()获得的 } // 2】若是节点hash值对应的数组位置为空,直接赋值 if ((p = tab[i = (n - 1) & hash]) == null) {// i = (n - 1) & hash 求hashmap数组下标并赋值给i,判断相对应的数组节点是否为空 tab[i] = newNode(hash, key, value, null); // 若是为空,直接增长一个节点,很简单 } else { //3】若是不为空的话 Node<K,V> e; // 对应下标的新的节点的node值 K k;//相对应下标的node值p的key //若是hash值同样,key值也同样(注意,由于hash值同样,key值可能不同;先对比hash值再对比key值的(由于对比hash值速度更快)),则直接替换。key值同样的操做 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) { // 3-1】key 值同样时怎么处理(链表长度为1) e = p; } else if (p instanceof TreeNode) { // 3-2】判断节点是否为树节点,若是是,则按红黑树的插入方式插入元素 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); } else { // 2-3】若是不是树节点,则按链表的方式插入元素(由于这个槽是非空的,这里的for循环就是对这些槽进行遍历以处理)(链表长度大于1) for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 3-3-1】下一个节点恒等于空,说明是放进链表的末尾嘛 p.next = newNode(hash, key, value, null); //3-3-1】在末尾放这个node值嘛 if (binCount >= TREEIFY_THRESHOLD - 1) // 3-3-1-1】若是这个条件成立,说明最后的尾节点已是7了,要树化了,这个方法名很是形象 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { // 若是在这个链表上,key和value都同样的话 break; } p = e; } } //改变value值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) {//注释里:if true, don't change existing value,可是put这方方法传过来的是false e.value = value; } afterNodeAccess(e); return oldValue; } } ++modCount; //若是大于阈值,则扩容 if (++size > threshold) { resize(); } afterNodeInsertion(evict); return null; }
resize():
1) 该方法会在HashMap的键值对达到“阈值”后进行数组扩容,而扩容时会调用resize()方法,此外,在jdk1.7中数组的容量是在HashMap初始化的时候就已经赋予,而在jdk1.8中是在put第一个元素的时候才会赋予数组容量,而put第一个元素的时候也会调用resize()方法。\
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;// 旧数组赋值给oldTab,表明扩容以前HashMap中的数组,也就是全部的旧桶,旧容量 int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap是指oldCapacity,表明扩容以前总桶数量 int oldThr = threshold;// 0】旧阈值 int newCap; //新容量,此次扩容以后总桶数量 int newThr = 0;//新阈值 if (oldCap > 0) { // 1】table扩容过 if (oldCap >= MAXIMUM_CAPACITY) { //若是超过最大容量,就再也不扩容,注意threshold = Integer.MAX_VALUE; threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {// 若是老容量扩大2倍仍不超过最大值,则新容量为原来的2倍 newThr = oldThr << 1; // double threshold } } else if (oldThr > 0) {// 2】initial capacity was placed in threshold;使用带有初始容量的构造器时走这里,table容量为初始化获得的threshold(多么巧妙的设计啊) newCap = oldThr; // 2-1】 } else { // 3】zero initial threshold signifies using defaults;new HashMap().put("",""):默认构造器走这里 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 4】不带有初始容量的构造器走这里 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]; // 就是在这里真正对HashMap里面的数组进行初始化的 table = newTab; // 若是一开始是new hashmap(),则不走下面这一步了,由于oldTal为空嘛 if (oldTab != null) { //对新扩容后的table进行赋值 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) { // 若是e是树节点,则按照树结构处理该分支 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); } else { // preserve order 若是e是链表节点,则按照链表结构处理该分支 Node<K,V> loHead = null, loTail = null;//此对象接收会放在原来位置 Node<K,V> hiHead = null, hiTail = null;//此对象接收会放在“j + oldCap”(当前位置索引+原容量的值) Node<K,V> next; do { // 这个do while 循环就是遍历链表的 next = e.next; // 这个判断是个精华,就是判断rehash是否须要移位:详见参考文件第四篇,下面有说 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; }
get方法:
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; // table副本 Node<K,V> first; // 相对应下标的那个数组 Node<K,V> e; int n; // table的数组长度 K k; // table必定不能大于0啊,不然就返回空啊 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // always check first node 若是hash值同样,并且key值也同样;高度注意:桶中第一项(数组元素)相等(是桶中第一项元素,是第一项):第一项特别判断,由于链表红黑树不影响 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; }
主要是看其区别 ,在xmind上面看吧
参考资料
6)JDK1.8源码(九)——java.util.LinkedHashMap 类
参考连接:
2)深刻理解HashMap的扩容机制 :从这里能够了解到,jdk7的扩容标准跟jdk8是不同的
3)JDK1.8源码(七)——java.util.HashMap 类 写得真的很用心
4)jdk8之HashMap resize方法详解(深刻讲解为何1.8中扩容后的元素新位置为原位置+原数组长度):很是感谢这个做者,终于看明白hahsmap的扩容了