java集合相关

1.2java集合框架
1.2.1Arraylist与LinkedList异同
1.是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全的;
2.底层数据结构:Arraylist底层使用的是Object数组;LinkedList底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,详细可读https://www.cnblogs.com/xingele0917/p/3696593.html)
3.插入和删除是否受元素位置影响:
1)ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置影响。
比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种 情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时 间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向 前移一位的操作。
2)LinkedList采用链表存储,所以插入、删除元素时间复杂度不受元素位置的影响,都是近似O(1)而数组近似O(n)
4.是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)
5.内存空间占用:ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)
补充内容:RandomAccess接口

public interface RandomAccess { 
}

查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个 标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 在binarySearch()方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用 indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法

 public static <T> 
 int binarySearch(List<? extends Comparable<? super T>> list, T key) {
           if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)            
        	return Collections.indexedBinarySearch(list, key);        
        else            
        	return Collections.iteratorBinarySearch(list, key);   
}

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关! ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随 机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。, ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不 是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!
下面再总结一下 list 的遍历方式选择
实现了RandomAccess接口的list,优先选择普通for循环 ,其次foreach,
未实现RandomAccess接口的list, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大 size的数据,千万不要使用普通for循环

补充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从 双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表,如 下图所示,同时下图也是JDK1.6之前LinkedList 底层使用的是双向循环链表数据结构。
在这里插入图片描述
1.2.2ArrayList与Vector区别
Vector类的所有方法都是同步的。可以由两个线程安全的访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
ArrayList不是同步的,所以在不需要保证线程安全时建议使用ArrayList
1.2.3HashMap的底层实现
JDK1.8之前
JDK1.8之前HashMap底层是数组和链表结合在一起使用也就是链表散列。HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同的话就通过拉链法解决冲突。
所谓扰动函数指的就是HashMap的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法,换句话说,使用扰动函数之后可以减少碰撞。
JDK1.8HashMap的hash方法源码:
JDK1.8的hash方法相比于JDK1.7hash方法更加简化,但是原理不变

static final int hash(Object key){
	int h;
	//key.hashCode():返回散列值也就是hashCode
	//^:按位异或
	//>>>:无符号右移,忽略符号位,空位都以0补齐
	return (key==null)?0:(h=key.hashCode())^(h>>>16);
}

如果key为null,则hash值为0;否则,hash值就是key的hashCode值异或key的hashCode无符号右移16位的结果(即hashCode值的高16位和低16 位的异或结果)。通过异或处理,避免了只靠低位数据计算hash值时导致的冲突,计算结果由高低位结合决定,可以使元素分布更均匀。
(语法:result = expression1 >>> expression2,>>> 运算符把 expression1 的各个位向右移 expression2 指定的位数。右移后左边空出的位用零来填充。移出右边的位被丢弃。例如:
var temp
temp = -14 >>> 2
变量 temp 的值为 -14 (即二进制的 11111111 11111111 11111111 11110010),向右移两位后等于 1073741820 (即二进制的 00111111 11111111 11111111 11111100)。)

对比一下 JDK1.7的 HashMap 的 hash 方法源码.

//jdk1.7
	final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
    // 先取key的hashCode再和hashSeed进行异或运算
        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

	//通过hash函数得到散列值之后,再通过indexFor方法获取实际的存储位置
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);  //保证获取的index一定在数组范围内
    }

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。

所谓“拉链法”就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
在这里插入图片描述
JDK1.8之后
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转 化为红黑树,以减少搜索时间。
在这里插入图片描述
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺 陷,因为二叉查找树在某些情况下会退化成一个线性结构。
推荐阅读:
《Java 8系列之重新认识HashMap》 :https://zhuanlan.zhihu.com/p/21673805
在这里插入图片描述
ashMap是基于Map接口实现的,它提供了Map中所有的操作,HashMap不能保证元素的顺序(即存储顺序跟添加顺序不一致);而且,不保证这个顺序不发生变化(当rehash时,可能发生变化)。
HashMap的性能受两个参数的影响:initial capacity 和 load factor。容量就是hash表中桶的数量,初始容量就是hash表被创建时的初始容量。加载因子是衡量hash表允许达到多满才可以自动扩容。当hash表中enries数量超过加载因子和当前容量的乘积时,hash表会进行rehash(也就是说,内部数据结构会重建),导致hash表中桶的数量近乎翻倍。
1.2.4HashMap和Hashtable的区别
1.线程是否安全:HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过syncronized修饰(如果你要保证线程安全就使用ConcurrentHashMap吧)
2.效率:因为线程安全的问题,HashMap要比HashTable效率高一点。另外,HashTable基本被淘汰,不要再代码中使用它。
3.对Null key和Null value的支持:HanshMap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null。但是再HashTable中put进的键值只要有一个null,直接抛出NullPointerException
4.初始容量大小和每次扩充容量大小的不同
1)创建时如果不指定容量初始值,HashTable默认初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16.之后每次扩容,容量变为原来的2倍。
2)创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充 为2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总 是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
5.底层数据结构:JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。HashTable没有这样的机制。
HasMap 中带有初始容量的构造函数

static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    int threshold;
    //The load factor for the hash table.
    final float loadFactor;
 
 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);  
   }    
public HashMap(int initialCapacity) {        
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
   }

看第一个构造函数即可,因为第二个也是通过第一个来实现的,只是加载因子是默认值0.75 。
接下来看tableSizeFor方法,下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。

    /**
     * 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;
    }

通过注释可以看到,我们传进来的初始容量值,经过一系列的无符号右移运算和位的或运算,最终给我们返回最接近我们指定的初始容量值的2的幂次方。
这里不需要考虑指定初始容量值为负数的情况,因为在构造函数中已经进行了判断。如果为负数,则程序将抛出异常。
为什么要先进行一步减1的操作呢?如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数),即当我们指定初始容量正好是2的幂次方时,比如8,经过上述一系列的运算,最终会返回16,然而最接近于指定值的2的幂次方就是它本身。为了解决这一问题,程序首先执行了减1操作。
下面看看这几个无符号右移操作:
如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)。
这里只讨论n不等于0的情况。
第一次右移
n |= n >>> 1;
由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。
第二次右移
n |= n >>> 2;
注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。
第三次右移
n |= n >>> 4;
这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。
以此类推
注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就32个1,但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。
在这里插入图片描述
tableSizeFor方法的返回值赋值给了变量threshold,this.threshold = tableSizeFor(initialCapacity);但是为什么不是这样呢this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;?(当HashMap的size到达threshold这个阈值时会扩容)。
但是,请注意,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。
下面我们说一下hashmap的扩容
根据这四个问题依次往下进行:
HashMap如何进行put操作?
hash值如何计算?
何时进行扩容?
如何扩容?
put方法
从HashMap的put方法入手,逐步深入:

	/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

从put方法的注释可以知道,当添加新的key-value键值对时,如果key值已经存在,则新value将会替换原value值(因为putVal方法的第三个参数为false)。
hash方法
根据key计算出key值对应的hash值,看hash函数(HashMap类提供的静态方法):

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

如果key为null,则hash值为0;否则,hash值就是key的hashCode值异或key的hashCode无符号右移16位的结果(即hashCode值的高16位和低16 位的异或结果)。通过异或处理,避免了只靠低位数据计算hash值时导致的冲突,计算结果由高低位结合决定,可以使元素分布更均匀。(上面也有写,上面还有jdk1.7的put方法中是如何计算hash值的)
接着看,获取到key的hash值之后,先来熟悉一下HashMap中两个Node的定义,因为接下来我们会看到它活跃的身影。
Node节点的定义
在Map中存储的每一个键值对都是以一个Map.Entry<K,V>的实现对象存储的,Map.Entry是一个接口,不能实例化,所以Map的不同实现类,存储键值对的方式也会有所不同,只要这个键值对的类实现了Map.Entry接口即可。
在HashMap中键值对的存储方式有两种:
其中一种是我们比较熟知的链表存储结构,也就是以HashMap.Node<K,V>类的对象方式存储的, Node类是HashMap的一个静态内部类,实现了 Map.Entry<K,V>接口。在调用put方法创建一个新的键值对时,会调用newNode方法来创建Node对象。

// 该方法很简单,只是调用了Node类的构造函数
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
}

(1)Node静态内部类:

	/**
* 该类只实现了 Map.Entry 接口,
* 所以该类只需要实现getKey、getValue、setValue三个方法即可
* 除此之外以什么样的方式来组织数据,就和接口无关了
*/
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // 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; }
 
        // 重写父类Object的hashCode方法,且该方法不可被自己的子类再重写
        // 返回:key的hashCode值和value的hashCode值进行异或运算结果
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
 
        // 实现接口定义的方法,且该方法不可被重写
        // 设值,返回旧值
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
 
        /*
         * 重写父类Object的equals方法,且该方法不可被自己的子类再重写
         * 判断相等的依据是,只要是Map.Entry的一个实例,并且键键、值值都相等就返回True
         */
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
}

HashMap中每一个键值对都是以一个HashMap.Node<K,V>对象实例进行存储的,当不同的键对应的多个键值对路由到元素数组的同一个位置时,这多个键值对对应的Node对象之间会以链表的方式组织起来。这是默认的组织方式。
当我们恰好要根据key寻找一个在链表上的对象的时候,就涉及到遍历链表,逐个调用key对象的equals方法来比对我们要查找的到底是哪个键值对了。可想当链表的长度越长,匹配的时间复杂度就越高,和链表的长度成正比。这也就是HashMap内部实现时会根据链表的长度超过限定的阈值时,会将链表结构转换为红黑树结构,用来提升查询性能。replacementTreeNode方法就是此时被调用(treeifyBin方法里)。TreeNode类也就是红黑树的节点对象。

// 该方法只是调用了TreeNode类的构造方法,依据当前节点信息构造一个树节点对象
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

(1)TreeNode静态内部类:

/**
 * 首先该类是HashMap类的一个静态内部类
 * 包内可见、不可被继承
 * 该类继承了LinkedHashMap.Entry,而LinkedHashMap.Entry继承了HashMap.Node
 * PS:要知道LinkedHashMap是HashMap的子类,然而目前的状况是HashMap作为父类,他的一个静态内部类(TreeNode)居然继承了子类LinkedHashMap的一个静态内部类
 *(LinkedHashMap.Entry),这个设计不太理解。
 * 红黑树是一个二叉树,父节点、左节点、右节点、红黑标识都是二叉树中的元素
 */
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next); // 此处调用 LinkedHashMap.Entry的构造方法
    }
 
    // 以下省略了很多方法,后文会逐步解析
}

接下来再看下LinkedHashMap.Entry类的定义

/**
 * 首先该类是LinkedHashMap的一个静态内部类
 * 包内可见、又因为没有final修饰符(所以HashMap中的TreeNode类才能继承到他)
 * 该类除了增加了before、after两个实例变量之外,没有任何的行为扩展,也就是说他的所有行为都继承自HashMap.Node
 * 该类也只有一个构造方法,且该构造方法就是通过调用HashMap.Node的构造方法构造一个HashMap.Node对象
 * PS:看到这里就更加不理解为何HashMap.TreeNode不直接继承HashMap.Node,而要绕个弯来继承LinkedHashMap.Entry,难道是为了使用before、after?可貌似也没有使用到。
 */
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

putVal方法
我们已经由key计算得到hash值了,接着来看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
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //判断table数组是否为空,如果table数组为空,则通过resize()方法创建一个新的table数组,
        //将这个新的数组赋值给tab数组,并获取新table数组的长度赋值给n
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        	//这里调用resize方法,其实就是第一次put时,对数组进行初始化。
            n = (tab = resize()).length;
        // 根据hash值获取桶下标中当前元素,如果为null,说明之前没有存放过与key相对应的value,直接插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果此位置上没有数据,则创建一个新节点,并将其放到数组对应下标中
            tab[i] = newNode(hash, key, value, nul l); 
        else {	
        	//处理hash碰撞的情况:
            Node<K,V> e; K k;// e 用来指向根据key匹配到的元素
            //当该位置old节点的hash值与待添加元素的hash值相等,
            //并且两者的key也相同(key的地址或者key的equals()任意一个相等就认为是key重复了)
            //hash碰撞,并且当前桶中的第一个元素即为相同key
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //则使一个新节点引用到old节点
                e = p;// 用e指向到当前元素          
            else if (p instanceof TreeNode)	//判断old节点是否是红黑树节点:
                //将最新的key、value插入到树中
                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
                            //此时链表已经有至少9个节点了(binCount>=7,说明已经遍历了至少8次,p至少已经至少到了第8个节点,因为新的节点已经挂到p的后面了,所以链表当前至少9个节点了。这里面判断用了>=,个人理解是一种必须的防御式判断,因为由于并发问题是有可能会导致超出8个的情况的)
                            // 将该链表上所有元素改为TreeNode方式存储(是为了增加查询性能,元素越多,链表的查询性能越差) 或者 扩容。
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果在遍历链表的过程中,发现有节点的hash值与新节点的hash值相等,
                    //并且两者的key也相同(key的地址或者key的equals()任意一个相等就认为是key重复了),
                    //则跳出当前for循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;// 跳出循环,因为找到了相同的key对应的元素
                    p = e;
                }
            }
            //hash碰撞的最终处理:
            if (e != null) { //说明找了和要写入的key对应的元素,根据情况来决定是否覆盖值
                V oldValue = e.value;//旧值
                //当onlyIfAbsent为false,或者原节点的value值为null时,使用新值覆盖旧值
                //添加元素时onlyIfAbsent默认是false
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 用于LinkedHashMap的回调方法,HashMap为空实现
                afterNodeAccess(e);// 元素被访问之后的后置处理, LinkedHashMap中有具体实现
                return oldValue;
            }
        }
	     // 执行到这里,说明是增加了新的元素,而不是替换了老的元素,所以相关计数需要累加

        //新键值对的添加属于"Structural modifications", modCount要自增
        ++modCount;
        //当键值对数量超过threshold阈值时,调用resize方法进行数组扩容
        if (++size > threshold)//当前map的元素个数递增
            resize();
        //用于LinkedHashMap的回调方法,HashMap为空实现
        afterNodeInsertion(evict); // 添加新元素之后的后后置处理, LinkedHashMap中有具体实现
        return null;
    }

我们简单总结一下putVal主要做了哪些事:
1.判断桶数组table是否为空,如果为空,则通过resize扩容的方式进行初始化。
2.根据(n-1) & hash得到桶数组的索引值,判断该位置是否已有节点元素。如果没有元素,则直接将新节点添加到此索引位置处。添加成功之后,如果HashMap中键值对数量超过threshold扩容阈值,则调用扩容方法。
3.如果索引位置处已有节点,则需要分析三种情况:
(1)当桶数组索引处的节点的hash值与新元素的hash值相等,并且两者的key值也相同时,新元素的value值将覆盖旧值;
(2)当桶数组索引处的节点是红黑树节点时,将新元素插入到红黑树中;
(3)当桶数组索引处的节点是链表节点时,遍历此链表到尾部,将新元素插入到链表尾部,插入成功之后再根据链表长度与树化阈值的比较判断是否需要进行树化。如果在遍历链表的过程中有某一节点的hash值与新元素hash相等,并且两者的key值也相同,则新元素的value值将覆盖旧值。
resize方法
接下来终于到了要讲的resize核心扩容方法。
resize方法:初始化数组或对数组进行两倍扩容。如果数组为null,则分配的内存与threshold中保存的初始容量大小一致(指定初始容量的构造函数初始化HashMap),如果threshold为0就按照默认值16进行分配( 无参构造函数实例化HashMap)。由于我们使用2次幂进行扩展,每个bin中的元素在新table数组的位置要么还是原位置,要么是原位置再移动2次幂的位置(即原位置+原数组容量的位置)。

	/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //原table数组容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //原扩容阈值
        int oldThr = threshold;
        //新table数组容量、扩容阈值
        int newCap, newThr = 0;
        //如果原table数组容量大于0
        if (oldCap > 0) {
        	//原数组容量已经达到最大容量(1<<30,即2的30次方)时,
        	//扩容阈值设置为Integer的最大值(2147483647),
        	//并返回原table数组
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果原数组容量小于最大容量,则原数组容量2倍扩容,
            //如果新数组容量<最大容量,并且原数组容量>=默认初始容量16,
            //则新扩容阈值为原扩容阈值的2倍
            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
            //进入此if中说明数组为空,且创建HashMap时使用了带参数的构造函数:
            //public HashMap(int initialCapacity)或者public HashMap(int initialCapacity, float loadFactor)
            //这两个构造函数最终都会执行this.threshold = tableSizeFor(initialCapacity);
            //tableSizeFor方法会返回最接近于initialCapacity的2的幂次方,此值即为初始数组容量
            newCap = oldThr;
        else {  // zero initial threshold signifies using defaults
            //进入此if说明数组为空,且创建HashMap时使用了无参构造函数,
            //则初始化数组容量大小为16,扩容阈值为0.75*16=12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
        	//由上述逻辑判断可知,进入此if有两种可能:
        	//第一种:在“if(oldCap>0)”条件中,且不满足其中的两个if条件
        	//第二种:满足“else if(oldThr>0)”
        	//第一种是扩容的情况,第二种是map进行第一次put的情况
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //初始化table或者扩容, 实际上都是通过新建一个table数组来完成的
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //确定容量的Node数组
        table = newTab;
        //如果原数组有数据,说明不是首次初始化数组,则会造成扩容,否则,直接返回table;
    //元素重新分布的问题,消耗性能。
        if (oldTab != null) {
            //循环遍历原数组,对非空元素进行处理
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //获取数组索引为j处的元素,当索引j处有元素时:
                if ((e = oldTab[j]) != null) {
                    //清除原数组索引j处对Node节点的引用
                    oldTab[j] = null;
                    //如果该存储桶里面只有一个bin, 就直接将它放到新表的目标位置即当前数组下标上有元素,但是没有子元素,
                    //也就是没有形成链,就只有一个元素 ,那么该元素放置的位置按照newTab[e.hash & (newCap - 1)] 来放置。
                    //它在新数组中的位置是通过e.hash & (newCap - 1)确定下来的
                    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,还不知道要干啥?那就接着往下看
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        //保存下一个Node节点
                        Node<K,V> next;
                        //遍历链表元素(数组索引j处)
                        do {
                            next = e.next;
                //其实是拿到hash的第4位,若等于0,则新位置=原索引位置
                            if ((e.hash & oldCap) == 0) {
                                //首先是loHead和loTail都指向e节点,
                                //然后下一个节点将插入到上一节点的后面
                                //最后将loTail指向新插入的节点
                                //(其实就是链表元素的插入操作而已)
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //新位置=原索引+原数组容量
                            else {
                                //首先是hiHead和hiTail都指向e节点,
                                //然后下一个节点将插入到上一节点的后面
                                //最后将hiTail指向新插入的节点
                                //(其实就是链表元素的插入操作而已)
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //对于索引j处的链表,根据在新数组中的索引位置,链表可能会拆分成两个链表:
                        //在新数组的位置正好是原索引位置处或者在原索引+原数组容量位置处
                        if (loTail != null) {
                            //对于不需要移动位置的链表(loHead为首,loTail为尾)
                            //设置尾节点loTail的下一节点为null
                            loTail.next = null;
                            //将此链表添加到新数组的原索引j处
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            //对于需要移动位置的链表(hiHead为首,hiTail为尾)
                            //设置尾节点hiTail的下一节点为null
                            hiTail.next = null;
                            //将此链表添加到新数组的原索引j + oldCap处
                            newTab[j + oldCap] = hiHead;
                        }
                        //至此,我们应该就能看出来loHead、loTail、hiHead、hiTail四个节点的作用了。
                        //如果索引j处是链表,则此链表按照在新数组的位置可能会被拆分成两个链表:
                        //(1)loHead、loTail链表:在新数组的位置还是原索引位置处
                        //(2)hiHead、hiTail链表:在新数组的位置=原索引+oldCap
                    }
                }
            }
        }
        return newTab;
    }

其中 <<:是逻辑左移,右边补0,符号位和其他位一样要移动。
数学意义:在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。
>>和>>>的区别
1.>>表示右移(有符号右移),如:15>>2的结果是3,-31>>3的结果是-4,左边以该数的符号位补充,移出的部分将被抛弃。转为二进制的形式可能更好理解(省略左边的三个字节),0000 1111(15)右移2位的结果是0000 0011(3),1110 0001(-31)右移3位的结果是1111 1100(-4)。
2、>>>也表示右移,但是是无符号右移,如:15>>>2的结果是3,-31>>>3的结果是536870908,移出的部分将被抛弃:
同样转为二进制的形式,00000000 00000000 00000000 00001111(15)右移2位的结果是00000000 00000000 00000000 00000011(3),
11111111 11111111 11111111 11100001(-31)右移3位的结果是00011111 11111111 11111111 11111100(536870908)。

链表拆分的示意图:
在这里插入图片描述
在这里插入图片描述
最后,再来讲讲resize方法中的(e.hash & oldCap) == 0
通过对resize方法的分析,我们可以知道,(e.hash & oldCap) == 0这个拆分条件把索引j处的链表拆分成了两个链表,这两个链表在新数组的位置只有两种可能:原索引位置处,或者是原索引+oldCap位置处。
首先我们先明确三个点:
(1)oldCap一定是2的幂次方,假设是2^m;
(2)newCap一定是oldCap的2倍,即2^(m+1);
(3)假设数组容量是n,根据key的hash值确定其在数组中的索引是由(n - 1) & hash计算得来的(其实仅用到了hash的低m位进行运算)。
为什么仅用到了hash的低m位?很简单,我们看下面的举例:
假设数组容量是16,即2^4,则m=4,16-1=15,15的二进制表示如下:
0000 0000 0000 0000 0000 0000 0000 1111
可以看到,除了低四位,其余高位都是0,与hash进行&运算之后对应位还是0,所以,我们假设某一节点的hash值的低四位用abcd表示(四个0、1的任意组合)。
那么,当数组容量两倍扩容变为32,即2^5,节点在新数组中的索引位置就变成了(32 - 1) & hash,其实是取了hash的低5位进行运算。31的二进制表示如下:
0000 0000 0000 0000 0000 0000 0001 1111
对于同一个Node,其hash值是不变的,低五位的取值无非是两种情况:
0abcd或者1abcd
对于0abcd这种情况,(16 - 1) & hash的计算结果与(32 - 1) & hash的计算结果是一致的,表明节点在新数组中的索引位置还是原索引位置处。
对于1abcd这种情况,1abcd = 0abcd + 10000 = 0abcd + oldCap,即节点在新数组中的索引位置=原索引+扩容前数组容量。
所以,虽然数组容量扩大一倍,但是对于同一个key,它在新旧数组中的索引位置是有联系的:要么一致,要么相差一个oldCap。而索引位置是否一致体现在hash的第4位(在此例中,我们把最低位称作第0位),如何拿到hash的第4位呢?我们只需要这样做:
hash & 0000 0000 0000 0000 0000 0000 0001 0000
总结归纳之后,它其实就等效于 hash & oldCap。
由此,我们可以得出一个结论:
如果 (e.hash & oldCap) == 0 ,则该节点在新表的下标位置与旧表一致,都为 j;
否则,该节点在新表的下标位置为 j + oldCap

如此巧妙,既省去了重新计算hash值的时间,同时又把冲突的节点随机分散到了不同的桶上。
再举个例子:
在这里插入图片描述
如果对于它们三个,扩容一倍后,就会变成下边这样:
在这里插入图片描述
再说一下(无链情况)

if (e.next == null)
   newTab[e.hash & (newCap - 1)] = e;

这行的意思是,如果当前数组下标上有元素,但是没有子元素,也就是没有形成链,就只有一个元素 ,那么该元素放置的位置按照newTab[e.hash & (newCap - 1)] 来放置。
如下图所示:
n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希值(也就是根据key1算出来的hashcode值)与高位与运算的结果。
在这里插入图片描述
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket(桶)了。

总结
put(K key, V value)操作的流程图
在这里插入图片描述
下面再介绍一下treeifyBin方法:
treeifyBin方法,应该可以解释为:把容器里的元素变成树结构。当HashMap的内部元素数组中某个位置上存在多个hash值相同的键值对,这些Node已经形成了一个链表,当该链表的长度大于等于9的时候(因为binCount >= TREEIFY_THRESHOLD - 1,binCount>=7,说明已经遍历了至少8次,p至少已经至少到了第8个节点,因为新的节点已经挂到p的后面了,所以链表当前至少9个节点了。),会调用该方法来进行一个特殊处理。

/**
 * tab:元素数组,
 * hash:hash值(要增加的键值对的key的hash值)
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
 
    int n, index; Node<K,V> e;
    /*
     * 如果元素数组为空 或者 数组长度小于 树结构化的最小限制
     * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
     * 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)
     * 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
     */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize(); // 扩容,可参见resize方法解析
 
    // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
    // 根据hash值和数组长度进行取模运算后,得到链表的首节点
    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); // 继续遍历链表
 
        // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
 
        // 把转换后的双向链表,替换原来位置上的单向链表
        if ((tab[index] = hd) != null)
            hd.treeify(tab);//此处单独解析
    }
}

原来MIN_TREEIFY_CAPACITY出现在了treeifyBin()中!这样做会让逻辑更清楚,避免了在treeifyBin()前判断capacity与MIN_TREEIFY_CAPACITY比较的代码量过大。

下面再介绍下treeify方法
(treeify方法是TreeNode类的一个实例方法,通过TreeNode对象调用,实现该对象打头的链表转换为树结构)
我们都知道,目前HashMap是采用数组+链表+红黑树的方式来存储和组织数据的。
在put数据的时候,根据键的hash值寻址到具体数组位置,如果不存在hash碰撞,那么这个位置就只存储这么一个键值对。
如果两个key的hash值相同,那么对应数组位置上就需要用链表的方式将这两个数据组织起来,当同一个位置上链表中的元素达到8个的时候,就会再将这些元素构建成一个红黑树,同时把原来的单链表结构变成了双链表结构,也就是这些元素即维持着红黑树的结构又维持着双链表的结构。当第9个相同hash值的键值对put过来时,发现该位置已经是一个树节点了,那么就会调用putTreeVal方法,将这个新的值设置到指定的key上。

/**
 * 当存在hash碰撞的时候,且元素数量大于8个时候,就会以红黑树的方式将这些元素组织起来
 * map 当前节点所在的HashMap对象
 * tab 当前HashMap对象的元素数组
 * h   指定key的hash值
 * k   指定key
 * v   指定key上要写入的值
 * 返回:指定key所匹配到的节点对象,针对这个对象去修改V(返回空说明创建了一个新节点)
 */
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                int h, K k, V v) {
    Class<?> kc = null; // 定义k的Class对象
    boolean searched = false; // 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点。
    TreeNode<K,V> root = (parent != null) ? root() : this; // 父节点不为空那么查找根节点,为空那么自身就是根节点
    for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,没有终止条件,只能从内部退出
        int dir, ph; K pk; // 声明方向、当前节点hash值、当前节点的键对象
        if ((ph = p.hash) > h) // 如果当前节点hash 大于 指定key的hash值
            dir = -1; // 要添加的元素应该放置在当前节点的左侧
        else if (ph < h) // 如果当前节点hash 小于 指定key的hash值
            dir = 1; // 要添加的元素应该放置在当前节点的右侧
        else if ((pk = p.key) == k || (k != null && k.equals(pk))) // 如果当前节点的键对象 和 指定key对象相同
            return p; // 那么就返回当前节点对象,在外层方法会对v进行写入
 
        // 走到这一步说明 当前节点的hash值  和 指定key的hash值  是相等的,但是equals不等
        else if ((kc == null &&
                    (kc = comparableClassFor(k)) == null) ||
                    (dir = compareComparables(kc, k, pk)) == 0) {
 
            // 走到这里说明:指定key没有实现comparable接口   或者   实现了comparable接口并且和当前节点的键对象比较之后相等(仅限第一次循环)
        
 
            /*
             * searched 标识是否已经对比过当前节点的左右子节点了
             * 如果还没有遍历过,那么就递归遍历对比,看是否能够得到那个键对象equals相等的的节点
             * 如果得到了键的equals相等的的节点就返回
             * 如果还是没有键的equals相等的节点,那说明应该创建一个新节点了
             */
            if (!searched) { // 如果还没有比对过当前节点的所有子节点
                TreeNode<K,V> q, ch; // 定义要返回的节点、和子节点
                searched = true; // 标识已经遍历过一次了
                /*
                 * 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了
                 * 这是个短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了
                 * find 方法内部还会有递归调用。参见:find方法解析
                 */
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                        (q = ch.find(h, k, kc)) != null))
                    return q; // 找到了指定key键对应的
            }
 
            // 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
            dir = tieBreakOrder(k, pk); // 再比较一下当前节点键和指定key键的大小
        }
 
        TreeNode<K,V> xp = p; // 定义xp指向当前节点
        /*
        * 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
        * 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
        * 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
        */
        if ((p = (dir <= 0) ? p.left : p.right) == null) {  
            // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
            Node<K,V> xpn = xp.next; // 获取当前节点的next节点
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建一个新的树节点
            if (dir <= 0)
                xp.left = x;  // 左孩子指向到这个新的树节点
            else
                xp.right = x; // 右孩子指向到这个新的树节点
            xp.next = x; // 链表中的next节点指向到这个新的树节点
            x.parent = x.prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
            if (xpn != null) // 如果原来的next节点不为空
                ((TreeNode<K,V>)xpn).prev = x; // 那么原来的next节点的前节点指向到新的树节点
            moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
            return null; // 返回空,意味着产生了一个新节点
        }
    }
}
 
 
/**
 * 参数为HashMap的元素数组
 */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null; // 定义树的根节点
    for (TreeNode<K,V> x = this, next; x != null; x = next) { // 遍历链表,x指向当前节点、next指向下一个节点
        next = (TreeNode<K,V>)x.next; // 下一个节点
        x.left = x.right = null; // 设置当前节点的左右节点为空
        if (root == null) { // 如果还没有根节点
            x.parent = null; // 当前节点的父节点设为空
            x.red = false; // 当前节点的红色属性设为false(把当前节点设为黑色)
            root = x; // 根节点指向到当前节点
        }
        else { // 如果已经存在根节点了
            K k = x.key; // 取得当前链表节点的key
            int h = x.hash; // 取得当前链表节点的hash值
            Class<?> kc = null; // 定义key所属的Class
            for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出
                // GOTO1
                int dir, ph; // dir 标识方向(左右)、ph标识当前树节点的hash值
                K pk = p.key; // 当前树节点的key
                if ((ph = p.hash) > h) // 如果当前树节点hash值 大于 当前链表节点的hash值
                    dir = -1; // 标识当前链表节点会放到当前树节点的左侧
                else if (ph < h)
                    dir = 1; // 右侧
 
                /*
                 * 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
                 * 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
                 * 如果还是相等,最后再通过tieBreakOrder比较一次
                 */
                else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
 
                TreeNode<K,V> xp = p; // 保存当前树节点
 
                /*
                 * 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
                 * 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
                 * 如果当前树节点不是叶子节点(一棵树当中没有子结点(即度为0)的结点称为叶子结点),那么最终会以当前树节点的左孩子或者右孩子 为 起始节点  再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
                 * 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
                 * 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
                 */
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp; // 当前链表节点 作为 当前树节点的子节点
                    if (dir <= 0)
                        xp.left = x; // 作为左孩子
                    else
                        xp.right = x; // 作为右孩子
                    root = balanceInsertion(root, x); // 重新平衡
                    break;
                }
            }
        }
    }
 
    // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
    // 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。
    moveRootToFront(tab, root); // 单独解析
}

TreeNode类的putTreeVal方法:
在putVal方法中用到,将最新的key、value插入到树中

HashMap常量设计目的
HashMap中有哪些常量?这些常量设计的目的是什么?本篇带你走近Doug Lea、Josh Bloch、Arthur van Hoff、 Neal Gafter对HashMap的设计。(以下都是基于jdk1.8)
常量设计
(1)HashMap默认初始化大小是1 << 4(即16)

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

关于这个变量,注释说“MUST be a power of two”,即必须是2的幂次方。为什么一定要是2的幂次方呢?
HashMap底层数据结构是数组+链表(或数组+红黑树),当添加元素时,索引定位使用的是i =(n - 1) & hash ,当初始化大小n是2的幂次方时,它就等价于 n % hash 。定位下标一般用取余法,而按位与(&)运算的效率要比取余(%)运算的效率高,所以默认初始化大必须为2的幂次方,就是为了使用更高效的与运算。
默认初始化大小为什么是16而不是8或者32?如果太小,扩容比较频繁;如果太大,又占用内存空间。这算是jdk为我们做的初始权衡吧。
(2)HashMap最大容量是1<<30,即2的30次方

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

我们知道int是占4个字节,一个字节是8位,所以说是32位整型,那按理说可以左移31位,即2的31次幂。在这里为什么不是2的31次方呢?实际上,二进制数的最左边那一位是符号位,用来表示正负的。我们来看下面的例子:

   System.out.println(1 << 30);
   System.out.println(1 << 31);
   System.out.println(1 << 32);
   System.out.println(1 << 33);

输出:

1073741824
-2147483648
1
2

所以,HashMap的最大容量就是2的30次方。
(3)HashMap默认加载因子是0.75

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

HashMap表征hash表的填满程度,让我们看一下源码对load factor的解释:

 * <p>As a general rule, the default load factor (.75) offers a good
 * tradeoff between time and space costs.  Higher values decrease the
 * space overhead but increase the lookup cost (reflected in most of
 * the operations of the <tt>HashMap</tt> class, including
 * <tt>get</tt> and <tt>put</tt>).  The expected number of entries in
 * the map and its load factor should be taken into account when
 * setting its initial capacity, so as to minimize the number of
 * rehash operations.  If the initial capacity is greater than the
 * maximum number of entries divided by the load factor, no rehash
 * operations will ever occur.

通常来说,加载因子的默认值0.75在时间性能和空间消耗之间达到了平衡。较高的值虽然降低了空间消耗,但是却增加了查找时间(反映在HashMap大多数的操作上,包括get和put)。当设置初始容量的时候,应该考虑将要放入map中的元素数量和加载因子,以减少rehash的次数。如果初始的容量比预计的entry数量除以加载因子的商还要大,那么永远不需要rehash操作。
(4)HashMap默认树化(链表转换成红黑树)阈值是8

    /**
     * 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;

Java8及以后的版本中,HashMap底层数据结构引入了红黑树,当添加元素的时候,如果桶中链表元素超过8,会自动转为红黑树。那么阈值为什么是8呢?来看HashMap源码中的这段注释:

	 * Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million

理想状态中,在随机哈希码情况下,对于默认0.75的加载因子,桶中节点的分布频率服从参数约为0.5的泊松分布,即使粒度调整会产生较大方差。从数据中可以看到链表中元素个数为8时的概率非常非常小了,所以链表转换红黑树的阈值选择了8。
(5)HashMap中一个树的链表还原阈值是6

    /**
     * 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;

链表树化阀值是8,那么树还原为链表为什么是6而不是7呢?这是为了防止链表和树之间频繁的转换。如果是7的话,假设一个HashMap不停的插入、删除元素,链表个数一直在8左右徘徊,就会频繁树转链表、链表转树,效率非常低下。
(5)HashMap的最小树化容量是64

     /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

为什么是64呢?这是因为容量低于64时,哈希碰撞的机率比较大,而这个时候出现长链表的可能性会稍微大一些,这种原因下产生的长链表,我们应该优先选择扩容而避免不必要的树化。
HashMap类注释解析
1.2.5HashMap的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均。我们上面也讲到了过了,Hash 值的 范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均松散,一般应 用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之 前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算 方法是“ (n - 1) & hash ”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其 除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
那么 a % b 操作为什么等于 a & ( b - 1 )呢? (前提是b等于2的n次幂)
举例说明:
若 a = 10 , b = 8 , 10与8取余应得2.
8的二进制为: 1000 ; 7的二进制为: 0111(最左位补个0).
也就是说-----2的n次幂减一这样的数的二进制都是如0000111111这样前半部分是0后半部分是1的形式.
所以, 用2的n次幂减一这样的数 & 另一个数就相当于 这个数取余 (%) 2的n次幂
补充:
& 和 && 相同点:
都表示“与”操作。这里的“与”和数学中的“与或非”中的“与”意义相同,都遵循“一假必假”原则。即“与”符号两边的元素只要有一个为假,“与"操作执行后的结果就为假。
& 和 && 的区别:
1)& 表示“按位与",这里的”位“是指二进制位(bit)。
例:十进制数字8 转化为二进制是:1000 ;数字9 转化为二进制是1001 。
则如有以下程序:
public class Test {
public static void main(String[]args) {
System.out.println(9 & 8);
}
}
输出结果应该是:8
原因:1001 & 1000 = 1000 。 计算机中一般1表示真,0表示假。最左边一位1&1=1,最右边一位1&0 = 0.
2) && 表示逻辑”与“ ,即java中的boolean值才可以存在于&&符号的左右两侧。
true && false = false ,true && true = true, 依旧是"一假必假”。
值的注意的是:&& 符号有所谓的“短路原则”,当 A && B 出现时,如果A经判断是假,那么B表达式将不会获得执行或被判断的机会。直接结果就为假。
:关于十进制与二进制的转换,简单的说每四位可以遵循”8421“原则,1001即8+1=9,1011即8+2+1=11
在这里插入图片描述
1.2.6HashMap多线程操作导致死循环问题
在多线程下,进行put操作会导致HashMap死循环,原因在于HashMap的扩容resize()方法,由于扩容是重建一个数组,复制原数据到数组。由于数组下标挂有链表,所以需要复制链表,但是多线程操作与可能导致环形链表。复制链表过程如下:
以下模拟2个线程同时扩容。假设,当前 HashMap 的空间为2(临界值为1),hashcode 分别为 0 和 1,在散列地址 0 处有元素 A 和 B,这时候要添加元素 C,C 经过 hash 运算,得到散列地址为 1,这时候由于超过了临界值,空 间不够,需要调用 resize 方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:
线程一:读取到当前的 HashMap 情况,在准备扩容时,线程二介入
在这里插入图片描述
线程二:读取 HashMap,进行扩容

在这里插入图片描述
线程一:继续执行
在这里插入图片描述
这个过程为,先将 A 复制到新的 hash 表中,然后接着复制 B 到链头(A 的前边:B.next=A),本来 B.next=null, 到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将 B.next=A,所以,这里继续复制A,让 A.next=B,由此,环形链表出现:B.next=A; A.next=B
注意:jdk1.8已经解决了死循环的问题
jdk1.8之前和之后不同之处就是jdk1.8后是直接把节点放到newtable[j]尾节点,而jdk1.8前是直接放到头节点。虽然解决了死循环,但hashMap在多线程使用下还是会有很多问题,在多线程下最好还是使用ConcurrentHashMap比较好。
1.2.7HashSet和HashMap的区别
HashSet底层就是基于HashMap实现的。(HashSet 的源码非常非常 少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是 直接调用 HashMap 中的方法。)
在这里插入图片描述
1.2.8ConcurrentHashMap和Hashtable的区别
ConcurrentHashMap和Hashtable的d区别主要体现在实现线程安全的方式不同。

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8采用的结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类 似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了 分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁 竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑 树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很 多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构, 但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全, 效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
    看1:https://www.cnblogs.com/yangfeiORfeiyang/p/9694383.html(synchronized和CAS)
    ConcurrentHashMap实现原理及源码分析:
    https://www.cnblogs.com/chengxiao/p/6842045.html
    HashTable:

1.2.9 ConcurrentHashMap线程安全的具体实现方式/底层具体实现
1.2.10 集合框架底层数据结构总结
Collection
1.List

  • Arraylist:Object数组
  • Vector:Object数组
  • LinkedList:双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)详细可读: https://www.cnblogs.com/xingele0917/p/3696593.html

2.Set

  • HashSet(无序、唯一):基于HashMap实现的,底层采用HashMap来保存元素
  • LinkedHashSet:LinkedHashSet继承于HashSet,并且其内部是通过LinkedHashMap来实现的。有点类似于我们之前说的LinkedHashMap其内部是基于Hashmap实现一样,不过还是有一点点区别的。
  • TreeSet(有序、唯一):红黑树(自平衡的排序二叉树)

3.Map

  • HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希 冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默 认为8)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和 链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以 保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看: https://www.imooc.com/article/22931
  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

集合相关内容源码见最上方链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值