一,概念分析
1.数组和链表的对比
数组:占用内存连续的空间,空间占用大,寻址容易,插入删除困难
链表:内存空间不连续,空间占用比较小,寻址困难,插入删除容易
2.散列表
整合数组和链表两者的特性,数组里面每一个位置都保存一个链表。hash也称为散列,基本原理就是把任意长度的输入,通过hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。
Hash的特点:
1.从hash值不可以反向推导出原始的数据
2.输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
3.哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值
4.hash算法的冲突概率要小
由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。
抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。

二,手写源码
1.属性
//如果没有指定长度,默认的长度static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//table的最大长度static final int MAXIMUM_CAPACITY = 1 << 30;//默认的加载因子,与table的扩容有关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;
由此可见,当数组四分之三的位置上长度上都有元素时,就会引发数组扩容,当数组长度大于64且单个桶位元素大于8个时就会导致桶位上的元素从链表转化为红黑树。当红黑树的元素少于6个时就会导致桶位上的红黑树退化为链表,为什么是6呢?防止不停地树化反树化。
2.构造器
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}
public HashMap(int initialCapacity, float loadFactor) {//其实就是做了一些校验//初始化值必须大于0if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);//如果初始化值大于最大值,就将容量改为最大值if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;//加载因子必须大于0并且是一个数字if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}
从这里我们可以看到,hashmap的构造器采用了套娃的模式,实际上执行的构造器是最下面的一次经历过条件判断的构造器。如果我们能确定集合的大概长度,尽量在创建的时候指定长度,防止map不停地扩容操作,降低效率。
3.内部类

static class Node<K,V> implements Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey() { return key; }public final V getValue() { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Entry<?,?> e = (Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}
4.hash
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
如果key==null,hash值就是0,hash值就等于key的hash值^h无符号右移16位。
异或:相同则返回0,不同返回1
这就是哈希map底层使用的扰动,为了让高16位也参与运算。防止哈希冲突。
5.put
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}
可以看到这里又开始套娃了,实际上执行的是putVal方法。
6.putVal
/*** 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* 当插入的key和原有的key一致时,根据这个属性判断value是否覆盖* @param evict if false, the table is in creation mode.* 如果为false,就扩容。* @return previous value, or null if none*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//tab:引用当前hash的散列表//p:当前散列表的元素//n:散列表的数组长度//i:表示路由寻址 结果Node<K,V>[] tab; Node<K,V> p; int n, i;//延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//最简单的一种情况:寻址找到的桶位 刚好是 null,这个时候,直接将当前k-v=>node 扔进去就可以了if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {//e:不为null的话,找到了一个与当前要插入的key-value一致的key的元素//k:表示临时的一个keyNode<K,V> e; K k;//表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作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 {//当前桶位上是多个以链表形式存在的node,而且链表的头结点与我们要插入的数据不一致for (int binCount = 0; ; ++binCount) {//一直遍历到链表最后,也没找到一个与要插入的元素一样的,所以直接放到链表末尾if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//当前链表长度达到了树化的标准if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);//树化break;}//条件成立的话,说明找到了相同key的node元素,需要进行替换操作if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//e不等于null,条件成立说明,找到了一个与你插入元素key完全一致的数据,需要进行替换if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount; //modCount:表示散列表结构被修改的次数,替换Node元素的value不计数//插入新元素,size自增,如果自增后的值大于扩容阈值,则触发扩容。if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
map.put(“yhd”,”yhd”);
底层执行的操作流程
1.获取key的哈希值
2.经过哈希值扰动函数,使这个哈希值更散列
3.构造出一个node对象
4.使用哪个路由寻址算法,找出node对象应该存放的数组位置
关于路由寻址算法:
(table.length-1)&node.hash
7.resize
final Node<K,V>[] resize() {//oldTab:引用扩容前的哈希表Node<K,V>[] oldTab = table;//oldCap:表示扩容之前table数组的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldThr:表示扩容之前的扩容阈值,触发本次扩容的阈值int oldThr = threshold;//newCap:扩容之后table数组的大小//newThr:扩容之后,下次再次触发扩容的条件int newCap, newThr = 0;//条件如果成立说明 hashMap中的散列表已经初始化过了,这是一次正常扩容if (oldCap > 0) {//扩容之前的table数组大小已经达到 最大阈值后,则不扩容,且设置扩容条件为 int 最大值。if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//oldCap左移一位实现数值翻倍,并且赋值给newCap, newCap 小于数组最大值限制 且 扩容之前的阈值 >= 16//这种情况下,则 下一次扩容的阈值 等于当前阈值翻倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//oldCap == 0,说明hashMap中的散列表是null//1.new HashMap(initCap, loadFactor);//2.new HashMap(initCap);//3.new HashMap(map); 并且这个map有数据else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//oldCap == 0,oldThr == 0//new HashMap();else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;//16newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12}//newThr为零时,通过newCap和loadFactor计算出一个newThrif (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;//说明,hashMap本次扩容之前,table不为nullif (oldTab != null) {for (int j = 0; j < oldCap; ++j) {//当前node节点Node<K,V> e;//说明当前桶位中有数据,但是数据具体是 单个数据,还是链表 还是 红黑树 并不知道if ((e = oldTab[j]) != null) {//方便JVM GC时回收内存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;//hash-> .... 1 1111//hash-> .... 0 1111// 0b 10000if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}
8.get
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}
同样的套娃原理,实际上执行的是getNode()
final Node<K,V> getNode(int hash, Object key) {//tab:引用当前hashMap的散列表//first:桶位中的头元素//e:临时node元素//n:table数组长度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) {//第一种情况:定位出来的桶位元素 即为咱们要get的数据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;}
9.remove
public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;}
jdk专业套娃removeNode()
/*** Implements Map.remove and related methods.** @param hash hash for key* @param key the key* @param value the value to match if matchValue, else ignored* @param matchValue if true only remove if value is equal* @param movable if false do not move other nodes while removing* @return the node, or null if none*/final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {//tab:引用当前hashMap中的散列表//p:当前node元素//n:表示散列表数组长度//index:表示寻址结果Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {//说明路由的桶位是有数据的,需要进行查找操作,并且删除//node:查找到的结果//e:当前Node的下一个元素Node<K,V> node = null, e; K k; V v;//第一种情况:当前桶位中的元素 即为 你要删除的元素if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {//说明,当前桶位 要么是 链表 要么 是红黑树if (p instanceof TreeNode)//判断当前桶位是否升级为 红黑树了//第二种情况//红黑树查找操作node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {//第三种情况//链表的情况do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}//判断node不为空的话,说明按照key查找到需要删除的数据了if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {//第一种情况:node是树节点,说明需要进行树节点移除操作if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);//第二种情况:桶位元素即为查找结果,则将该元素的下一个元素放至桶位中else if (node == p)tab[index] = node.next;else//第三种情况:将当前元素p的下一个元素 设置成 要删除元素的 下一个元素。p.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null;}
10.replace
@Overridepublic boolean replace(K key, V oldValue, V newValue) {Node<K,V> e; V v;if ((e = getNode(hash(key), key)) != null &&((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {e.value = newValue;afterNodeAccess(e);return true;}return false;}
本文参照哔哩哔哩小刘讲源码和jdk的HashMap源码。
