前言
1、为什么要使用HashMap?
2、细嚼HashMap主要参数2.1、静态常量 //默认的初始化桶容量,必须是2的幂次方(后面会说为什么) staticfinal int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大桶容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //判断是否将链表转化为树的阈值 static final int TREEIFY_THRESHOLD = 8; //判断是否将树转化为链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; //判断是否可以执行将链表转化为树,如果当前桶的容量小于此值,则进行resize()。避免表容量过小,较容易产生hash碰撞。 static final int MIN_TREEIFY_CAPACITY = 64;2.2、字段 //hash表 transient Node<K,V>[] table; //缓存的EntrySet,便与迭代使用 transient Set<Map.Entry<K,V>> entrySet; //记录HashMap中键值对的数量 transient int size; //当对hashMap进行一次结构上的变更,会进行加1。结构变更指的是对Hash表的增删操作。 transient int modCount; //判断是否扩容的阈值。threshold = capacity * load factor int threshold; //负载因子,用于计算threshold,可以在构造函数时指定。 final float loadFactor;3、嗅探HashMap数据结构
- 数组只需对 [首地址+元素大小*k] 就能找到第k个元素的地址,对其取地址就能获得该元素。
- CPU缓存会把一片连续的内存空间读入,因为数组结构是连续的内存地址,所以数组全部或者部分元素被连续存在CPU缓存里面。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; //key 的hash final K key; //key对象 V value; //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; } }

4、走进HashMap构造函数
public HashMap(int initialCapacity) { //如果只传入初始值,则负载因子使用默认的0.75 this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //保证初始容量最大为2^30 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); }
/** * Returns a power of two size for the given target capacity. */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; }
5、解剖HashMap主要方法5.1、put
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}static final int hash(Object key) { int h; //将key的高16位与低16位异或,减小hash碰撞的机率 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
/** * 此方法用于将(k,v)键值对存储到HashMap中 * * @param hash key的hash * @param key key对象 * @param value key对应的value对象 * @param onlyIfAbsent 如果是true,则不覆盖原值。 * @param evict if false, the table is in creation mode. * @return 返回旧值,如果没有,则返回null。 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //在第一次put的时候,此时Node表还未初始化,上面我们已经知道,构造HashMap对象时只是初始化了负载因子及初始容量,但并没有初始化hash表。在这里会进行第一次的初始化操作。 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果得到了一个hash值,并且hash值在很少相同的情况下,如何均匀的分布到table数组里呢?最容易想到的就是用hash%n,n为table数组的长度。但是%运算是很慢的,我们知道位运算才是最快的,计算机识别的都是二进制。所以如果保证n为2的幂次方,hash%n 与 hash&(n-1)的结果就是相同的。这就是为什么初始容量要是2的幂次方的原因。 //当找到的hash桶位没有值时,直接构建一个Node进行插入 if ((p = tab[i = (n - 1) & hash]) == null) tab = newNode(hash, key, value, null); else { //否则,表明hash碰撞产生。 Node<K,V> e; K k; //判断hash是否与桶槽的节点hash是否相同并且key的equals方法也为true,表明是重复的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 { //表明是链表,还未转换为红黑树。 for (int binCount = 0; ; ++binCount) { //如果节点的next索引是null,表明后面没有节点,则使用尾插法进行插入 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //此时链表长度为9,即hash碰撞8次,会将链表转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果key是同一个key,则跳出循环链表 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //判断是否是重复的key if (e != null) { // existing mapping for key //拿到旧值 V oldValue = e.value; //因为put操作默认的onlyIfAbsent为false,所以,默认都是使用新值覆盖旧值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //返回旧值 return oldValue; } } //到这里,表明有新数据插入到Hash表中,则将modCount进行自增 ++modCount; //判断当前键值对容量是否满足扩容条件,满足则进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

- put方法先通过计算key的hash值;
- 如果hash表没有初始化,则进行初始化;
- 然后计算该hash应该处于hash桶的哪个位置;
- 如果该位置没有值,则直接插入;
- 如果有值,判断是否为树节点,是的话插入到红黑树中;
- 否则则是链表,使用尾插法进行插入,插入后判断hash碰撞是否满足8次,如果满足,则将链表转化为红黑树;
- 插入后判断key是否相同,相同则使用新值覆盖旧值;
- 进行++modCount,表明插入了新键值对;再判断是否进行扩容。、