哈希表(hash table)也叫散列表,是一种很是重要的数据结构
应用场景之一:缓存技术(好比memcached的核心其实就是在内存中维护一张大的哈希表)node
用一段连续的存储单元来存储数据。
知道下标进行查找,时间复杂度为O(1)。
知道value值进行查找,时间复杂度为O(n),由于须要遍历数组,逐一比对给定关键字和数组元素。
对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提升为O(logn)。
插入、删除操做,涉及到数组元素的移动,其平均复杂度也为O(n)。数组
新增,删除等操做(在找到指定操做位置后),仅需处理结点间的引用便可,时间复杂度为O(1)。
查找操做须要遍历链表逐一进行比对,复杂度为O(n)。缓存
一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操做,平均复杂度均为O(logn)。安全
不考虑哈希冲突的状况下,添加,删除,查找等操做,仅需一次定位便可完成,时间复杂度为O(1)。数据结构
数据结构的物理存储结构:并发
一、顺序存储结构
二、 链式存储结构app
哈希表的主干就是数组。利用了数组的特性----根据下标查找某个元素一次定位就能够找到。
在新增或查找某个元素时,咱们经过把当前元素的关键字传给哈希函数,而后映射到数组中的某个位置,最后经过数组下标一次定位就可完成操做。memcached
存储位置 = f(关键字)
这个函数的设计好坏会直接影响到哈希表的优劣。函数
插入、查找操做,如图:性能
若是两个不一样的元素,经过哈希函数得出的实际存储地址相同怎么办?
好的哈希函数会尽量地保证 计算简单和散列地址分布均匀,可是,咱们须要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证获得的存储地址绝对不发生冲突。
哈希冲突的解决方案:
一、开放定址法
二、再散列函数法
三、链地址法
HashMap便是采用了链地址法,也就是数组+链表的方式。
JDK 8 中,HashMap的主干是一个Node数组。
//该table在第一次使用时初始化,并在必要时进行调整。当分配时,长度老是2的幂。 transient Node<K,V>[] table;
Node是HashMap中的一个静态内部类
//HashMap.Node是LinkedHashMap.Entry的父类 //LinkedHashMap.Entry是HashMap.TreeNode的父类 static class Node<K,V> implements Map.Entry<K,V> { final int hash;//对key的hashcode值进行hash运算后获得的值,存储在Entry,避免重复计算 final K key; V value; Node<K,V> next;//存储指向下一个Node的引用,单链表结构 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
HashMap的总体结构以下:
HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,若是定位到的数组位置不含链表(当前Node的next为null),那么对于查找,添加等操做很快,仅需一次寻址便可;若是定位到的数组位置包含链表,对于添加操做,其时间复杂度为O(n),首先遍历链表,存在即覆盖,不然新增;对于查找操做来说,仍需遍历链表,而后经过key对象的equals方法逐一比对查找。因此,性能考虑,HashMap中的链表出现越少,性能才会越好。
HashMap的几个重要属性
//实际存储的key-value键值对的个数 transient int size; //阈值; //当table分配内存空间后,threshold通常为 capacity*loadFactory //HashMap在进行扩容时须要参考threshold int threshold; //负载因子,表明了table的填充度有多少,默认是0.75,超过了负载,就开始扩容 final float loadFactor; //用于快速失败,因为HashMap非线程安全,在对HashMap进行迭代时,若是期间其余线程的参与致使HashMap的结构发生变化了(好比put,remove等操做),须要抛出异常ConcurrentModificationException transient int modCount;
HashMap有4个构造器,若是用户没有给构造器传入initialCapacity 和loadFactor这两个参数,会使用默认值 initialCapacity默认为16,loadFactory默认为0.75。
在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操做的时候才真正构建table数组。
public V put(K key, V value) { return putVal(hash("2"), "2", "ljs", false, true); }
static final int hash(Object key) { int h; //key.hashCode()该对象本身的hashcode //HashMap的哈希函数:(hashcode) ^ (hashcode >>> 16) //hashcode 与 向右无符号移动16位的本身 异或,通常都等于hashcode的值 // >>> 与 >> 都是右移,>>> 是会把符号位也一块儿移动,就是说负数用 >>> 后,会成为正数 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
/** * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是table数组为空数组{},为table分配实际内存空间;----resize() //在构造器中没有指定threshold的话,就是默认的threshold,16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash key的哈希值 和 数组长度作 与运算,计算出在table数组中的具体下标位置 //该位置没有数据,就直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //该位置有数据,遍历该数组下标的单链表 //找到hash、key相同的,执行覆盖操做。用新value替换旧value,并返回旧value //没有hash、key相同的,插入到链表尾部 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;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败 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; //不是第一次resize(),扩容----Threshold * 2 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; //第一次resize(),为table分配内存空间,newCap = 16 ; newThreshold = 0.75*16 = 12 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
经过以上代码可以得知,当size大于阈值的时候,须要进行数组扩容,扩容时,须要新建一个长度为以前数组2倍的新的数组,而后将当前的Node数组中的元素所有传输过去,扩容后的新数组长度为以前的2倍,因此扩容相对来讲是个耗资源的操做。
存储位置的肯定流程:
key.hashcode()-->hash()-->(length - 1) & hash-->最终索引位置,找到对应位置table[i]。
public V get("2") { Node<K,V> e; return (e = getNode(hash("2"), "2")) == null ? null : e.value; }
static final int hash("2") { int h; return ("2" == null) ? 0 : (h = "2".hashCode()) ^ (h >>> 16); }
final Node<K,V> getNode(50, "2") { 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; }
取值位置的肯定流程:
key.hashcode()-->hash()-->(length - 1) & hash-->最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,经过key的equals方法比对查找对应的记录。
注:存数据须要hashcode(),取数据须要equals();hashcode()、equals()是Object的方法,能够按照本身的需求,重写对象的hashcode() 和 equals() 方法。
数组进行扩容,数组长度发生变化,而存储位置 index = h&(length-1),index也可能会发生变化,须要从新计算index:
将老数组中的数据逐个链表地遍历,扔到新的扩容后的数组中,咱们的数组索引位置的计算是经过 对key值的hashcode进行hash函数运算后,再经过和 length-1进行与运算。
16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。从下图能够咱们也能看到这样会保证低位全为1,而扩容后只有一位差别,也就是多出了最左位的1,这样在经过 h & (length-1)的时候,只要h对应的最左边的那一个差别位为0,就能保证获得的新的数组索引和老数组索引一致(大大减小了以前已经散列良好的老数组的数据位置从新调换)。
数组长度保持2的次幂,length-1的低位都为1
&运算,高位是不会对结果产生影响的,因此只关注低位,若是低位所有为1,那么对于h低位部分来讲,任何一位的变化都会对结果产生影响,也就是说,要获得index=21这个存储位置,h的低位只有这一种组合。
若是不是2的次幂,也就是低位不是全为1此时,要使得index=21,h的低位部分再也不具备惟一性了,哈希冲突的概率会变的更大,同时,index对应的这个bit位不管如何不会等于1了,而对应的那些数组位置也就被白白浪费了