JDK核心源码深入剖析(双向链表和哈希映射)

JDK核心源码深入剖析(双向链表和哈希映射)

1 双向链表底层实现原理

1.1 双向链表与数据结构

什么是LinkedList
LinkList是一个双向链表(双链表);它是链表的一种,也是最常见的数据结构,其内部数据呈线性排列,属于线性表结构.
在这里插入图片描述

它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,所以是双向链表.
LinkList特点:
在这里插入图片描述
链表: 优势:不是连续的内存,随便插入(前、中间、尾部) 插入O(1) 劣势:查询慢O(N)

线程不安全的,允许为null,允许重复元素
蓝色表示;可随意插入、删除
查询循环循环链表

总结
双链表既有指向下一个节节点的指针,也有指向上一个结点的指针(双向读)
所谓指针,就是指向其他节点的一个对象的引用(说白了就是定义了两个成员变量)
双向链表线程不安全的,允许为null,允许重复元素
查询O(n)
插入删除O(1)

1.2 双向链表继承关系

在这里插入图片描述
LinkedList 是一个继承于AbstractSequentialList的双向链表。 LinkedList 实现 List 接口,能对它进行队列操作。 LinkedList 实现 Deque 接口,能将LinkedList当作**双端队列(double ended queue)**使用。 LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。 LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。

1.3 双向链表源码深度剖析

  public static void main(String[] args) {
        LinkedList<String> linkedList = new LinkedList<String>();
        linkedList.add("100");//尾插,等价于 linkedList.addLast()
        linkedList.add("200");
        linkedList.add("300");
       //*******中间插入linkedList..add(3,"700")*************
        linkedList.add("400");
        linkedList.add("500");
        linkedList.add("600");
        System.out.println(linkedList);
        linkedList.add(3,"700");//中间插入
        System.out.println(linkedList);
        //*******修改***************************************
        linkedList.set(3,"700000000");
        System.out.println(linkedList);
        //*******查询***************************************
        System.out.println(linkedList.getFirst());//头查
        System.out.println(linkedList.getLast());//尾插
//        for(int s=0;s<linkedList.size();s++){
//            System.out.println(linkedList.get(s));//随机插
//        }

        //*******移除***************************************
        LinkedList<String> linkedListRemove = new LinkedList<String>();
        linkedListRemove.add("100");
        linkedListRemove.add("200");
        linkedListRemove.add("300");
        linkedListRemove.remove(1);//指定移除
        linkedListRemove.removeAll(linkedList);//也调用上面的unlink方法;LinkedList.ListItr.remove


    }
1.3.1 链表成员变量与内部类

我们先来定义几个叫法,后面会用到它
在这里插入图片描述

    transient int size = 0; //元素个数

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first; //头结点引用(查询时获取)

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last; //尾节点引用(查询时获取)

  


    private static class Node<E> { //链表节点元素,封装了真实数据,同时加入了前后指针
        E item; ;//元素,这是放入的真实数据
        Node<E> next; //下一个节点,指针也是Node类型
        Node<E> prev; //上一个节点
        //构造器,前、值、后,很清晰
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;  //新元素
            this.next = next;  //下个节点
            this.prev = prev;  //上个节点
        }
    }
1.3.2 双向链表构造器

无参构造器: 没有做任何事情

public LinkedList() { //无参构造器 
}

有参构造器:传入外部集合的构造器

public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

秘密就藏在addAll上(重点,画图展示)

    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);   //边界判断

        Object[] a = c.toArray();  //不管你传啥类型,统一转成数组
        int numNew = a.length;  //需要新插入的个数
        if (numNew == 0)
            return false;
        //两个指针,这俩表示你要插入点的前后两个节点。我们称之为前置node和后置 node        //比如你的index=2 : 【 000 1111(pred) (index) 2222(succ) 33333 …… 】
        Node<E> pred, succ;
        if (index == size) { //下面就要定位到这俩指针的位置
            succ = null;  //如果指定的index和尾部相等,很显然后置是没有的
            pred = last;  //前置就是最后一个元素last
        } else {
            succ = node(index); //否则的话,后置就是当前index位置的node
            pred = succ.prev;  //前置就是当前index位置的prev,很好理解
        }

        for (Object o : a) {   //开始循环遍历插入元素
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null); //定义个新节点,包装当前 元素  
            if (pred == null)//如果前置为空,注意什么时候才为空?只有当前list没有元 素的时候
                first = newNode; //说明是第一次放入元素,将first指向当前元素,完工
            else
                pred.next = newNode; //否则的话,前置node的后指针指向当前元素(接上 了)
            pred = newNode; //让前置指针后移,指向刚新建的node,为下一次循环做准备
        } //依次循环,往上接,接完后,pred就是最后一个插入的元素

      //全部循环接完以后,再来处理新接链条的后指针
        if (succ == null) { //如果后置是null的话,说明我们一直在尾部插入
            last = pred; //将last指向最后一个插入的元素即可,它就是尾巴
        } else {
            pred.next = succ; //否则的话,最后一个插入的next指向原来插入前的后置
            succ.prev = pred; //后置的前指针指向最后插入的元素,这两步是一对操作缺一不可
        }  //到此为止,截断的后半截链条也对接上了。

        size += numNew; //最后不要忘记,元素数量增加
        modCount++; //操作计数器增加
        return true;
    }
1.3.3 链表插入(重点)

1) 双向链表尾插法
1、add(E e),
2、addLast;
调用的方法都一样(linkLast)

 public boolean add(E e) {
        linkLast(e); //在链表尾部添加
        return true;
    }

在链表尾部添加

  void linkLast(E e) {
        final Node<E> l = last; ;//取出当前最后一个节点
        final Node<E> newNode = new Node<>(l, e, null); //创建一个新节点,注意其前驱 节点为l
        last = newNode; //尾指针指向新节点
        if (l == null) //如果原来的尾巴节点为空,则表示链表为空,则将first节点也赋值为 newNode
            first = newNode;
        else
            l.next = newNode; // 否则的话,将原尾巴节点的后指针指向新节点,构成双向环
        size++; // 计数
        modCount++; // 计数
    }

结论:默认add就是尾插法,追加到尾部

2)双向链表中间插入

 linkedList.add(3,"700");//中间插入 

双向链表中间插入add(int index, E element)

//自定义插入 
linkedList.add(3,"700");

源码如下

 public void add(int index, E element) {
        checkPositionIndex(index); //越界检查

        if (index == size)  )//如果index就是指向的尾部,自然调尾插即可
            linkLast(element);
        else
            linkBefore(element, node(index)); //否则的话,找到index位置的node,插队到 它前面去
    }
 Node<E> node(int index) {
        // assert isElementIndex(index);
      // 这里有一个讨巧的设计!很灵活的应用了我们的first和last
        if (index < (size >> 1)) { // index如果小于链表长度的1/2 (size右移1就是除以 2)
            Node<E> x = first;
            for (int i = 0; i < index; i++) //从链表头开始移动 index 次
                x = x.next; //依次往后指
            return x;  // 循环完后,就找到了index位置的node,返回即可
        } else {  // 否则,说明index在链表的后半截,我们从链表尾部倒着往前找
            Node<E> x = last; 
            for (int i = size - 1; i > index; i--) //一直循环,直到index位置
                x = x.prev;
            return x;  //抓到后返回,完工
        }
    }

 void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        //找到之后,也就是这里的succ,我们就开始在它 前面插入新元素
        final Node<E> pred = succ.prev;  //上个节点
        final Node<E> newNode = new Node<>(pred, e, succ);  //构建新的双向节点
        succ.prev = newNode;  //修改后置节点的前指针
        if (pred == null)  //如果前驱节点为空,链表为空
            first = newNode;  //那么当前插入的就是头节点
        else
            pred.next = newNode; //否则修改前置的后指针,指向新节点,双向链表对接成功!
        size++;  //个数加1
        modCount++;  //修改次数加1
    }

1.3.4 双向链表修改方法

非常简单!
找到包装值的node,修改掉里面的属性即可

 public E set(int index, E element) {
        checkElementIndex(index); //越界检查
        Node<E> x = node(index);  //通过链表索引找到node
        E oldVal = x.item;//获取原始值
        x.item = element; //新值赋值
        return oldVal; //返回老值
    }
1.3.5 双向链表查询方法

简单!
get(int index):按照下标获取元素; 通用方法 getFirst():获取第一个元素; 特有方法,直接拿指针就是 getLast():获取最后一个元素; 特有方法,同样直接拿指针

  public E get(int index) {
        checkElementIndex(index);
        return node(index).item; //找到原始数组对应index的node
    }
1.3.6 双向链表删除方法

remove(E e):移除指定元素; 通用方法
removeAll(Collection<?> c) 移除指定集合的元素; 也调用的unlink方法

   public boolean remove(Object o) {
        if (o == null) {  //如果要移除null元素
            for (Node<E> x = first; x != null; x = x.next) { 
            //从fist顺着链表往后 找
                if (x.item == null) { //发现就干掉
                    unlink(x);   //重点!干掉元素调用的其实是unlink方法
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
             //如果不是移除null的话,路子一个样,无非就是 ==换成equals判断
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
 E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item; //元素
        final Node<E> next = x.next;//下个节点
        final Node<E> prev = x.prev;//上个节点

        if (prev == null) {
        //上个为空,说明当前要移除的就是头节点,将fist指针指向后置,我被 移除后它升级为头了
            first = next;
        } else {
            prev.next = next; //否则,前置的后指针指向后置
            x.prev = null; //当前节点的前指针切断!
        }

        if (next == null) { //后置为空说明当前要移除的是尾节点,我被移除后,我的前置成为尾巴
            last = prev;
        } else {
            next.prev = prev; //否则,后置的前指针指向前置节点
            x.next = null; //当前节点的后指针切断!
        } //到这里前后指针就理清了,该断的断了,该接的接了

        x.item = null; // 把当前元素改成null,交给垃圾回收
        size--; //链表大小减一
        modCount++; //修改次数加一
        return element; //已删除元素
    }

2 不可不知的哈希映射

2.1 HashMap数据结构

概念
HashMap 是一个利用**散列表(哈希表)**原理来存储元素的集合,是根据Key value而直接进行访问的数
据结构
在 JDK1.7 中,HashMap 是由 数组+链表构成的。
在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成

在这里插入图片描述
数组: 优势:数组是连续的内存,查询快(o1) 劣势:插入删除O(N)
链表: 优势:不是连续的内存,随便插入(前、中间、尾部) 插入O(1) 劣势:查询慢O(N)

思考?
为什么是JDK1.8 是数组+链表+红黑树???

HashMap变化历程
1.7的数据结构
:链表变长,效率低 了!!!
在这里插入图片描述
1.8的数据结构:
数组+链表+红黑树

在这里插入图片描述
链表–>红黑(链长度>8、数组长度大于64)

总结:
JDK1.8使用红黑树,其实就是为了提高查询效率
因为,1.7的时候使用的数组+链表,如果链表太长,查询的时间复杂度直接上升到了O(N)

2.2 HashMap继承体系

在这里插入图片描述
总结
HashMap已经继承了AbstractMap而AbstractMap类实现了Map接口

那为什么HashMap还要在实现Map接口呢?
据 java 集合框架的创始人Josh Bloch描述,这样的写法是一个失误。
在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。
显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来

  • Cloneable 空接口,表示可以克隆
  • Serializable 序列化
  • AbstractMap 提供Map实现接口

2.3 HashMap源码深度剖析

1)目标:
通过阅读HashMap(since1.2)源码,我们可以知道以下几个问题在源码是如何解决的
(1)HashMap的底层数据结构是什么?
(2)HashMap中增删改查操作的底部实现原理是什么?
(3)HashMap是如何实现扩容的?
(4)HashMap是如何解决hash冲突的?
(5)HashMap为什么是非线程安全的?

2)测试代码如下

 public static void main(String[] args) {
        HashMap<Integer, String> m = new HashMap<Integer, String>();//尾插
        //断点跟踪put
        m.put(1, "001");
        m.put(1, "002");
        m.put(17, "003");//使用17可hash冲突(存储位置相同)
        //断点跟踪get
        System.out.println(m.get(1));//返回002(数组查找)
        System.out.println(m.get(17));//返回003(链表查找)
        //断点跟踪remove
        m.remove(1);//移除
        System.out.println(m);
        m.remove(1, "002");//和上面的remove走的同一个代码
    }

3)关于hashMap基本结构的验证
先来个小验证,几乎地球人都知道map是 数组 + 链表 结构,那我们先来验证一下

在这里插入图片描述

再来看debug结果:
在这里插入图片描述
验证了基本结构,那为啥1和17就在一块了?到底谁和谁放在一个链上呢?内部到底怎么运作的?往下看 ↓

2.3.1 成员变量与内部类

目标:先了解一下它的基本结构
回顾:位运算(下面还会频繁用到)

1<<4
二进制相当于1右边补4个0:10000
十进制相当于1 x 2的4次方 , 也就是 16
二进制运算是因为它的计算效率高
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //16,默认数组容量:左 位移4位,即16

    /**
     * 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.
     */
     //最大容量:即2的30次幂
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
     //负载因子:扩容使用,统计学计算出 的最合理的
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

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

    /**
     * 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.
     */
     //当链表的值小<6, 红黑树转链表
    static final int UNTREEIFY_THRESHOLD = 6;

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


   //HashMap中的数组,中间状态数据
   transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
     //用来存放缓存,中间状态数据;
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
     //size为HashMap中K-V的实时数量(重点),注意!不是table的长度!
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
     //用来记录HashMap的修改次数,几个集合里都有它
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    //扩容临界点;(capacity * loadFactor)(重点)
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
     //负载因子 DEFAULT_LOAD_FACTOR = 0.75f赋值
    final float loadFactor;

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
    //数组和链表上的节点,1.8前叫 Entry
        final int hash; //扰动后的hash
        final K key; //map的key
        V value; //map的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;
        }

2.3.2 HashMap构造器

1)目标:学习下面的三个构造器,它们都干了哪些事情?

 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) {
 //赋值,多了一些边界判断
        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);
    }

map的构造函数就做了几个赋值这么点事?这么简单?错!接着往下看

2)无参构造函数验证
使用默认构造函数时,在put之前和之后分别debug以上变量信息对比看看
之前:
在这里插入图片描述
之后
在这里插入图片描述
3)自定义初始化参数验证
接下来我们胡搞一下,让容量=15,因子=0.5,猜一猜会发生什么?
在这里插入图片描述
调试到put之后,再来看:
在这里插入图片描述
源码剖析:
在有参数构造时,最终tableSizeFor

//capacity函数,初始化了table,就是table的length,否则取的是threshold
  final int capacity() {
        return (table != null) ? table.length :
            (threshold > 0) ? threshold :
            DEFAULT_INITIAL_CAPACITY;
    }

//带参数的初始化,其实threshold调用的是以下函数: 
//这是什么神操作??? 
//其实是将n转成2进制,右移再和自己取或,相当于把里面所有的0变成了1 
//最终目的:找到>=n的,1开头后面全是0的数。如果n=111 , 那就是 1000 ; 如果 n=100,那就是它自己 
//而这个数,恰好就是2的指数,为后面的扩容做铺垫 
 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; //到这一步n已经各个位都是1了。
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; //范围校验,小于0返回1,大于最大值返回最大值,绝大多数正常情况下,返回n+1,也就是 10000……
    }

调试案例:

  public static void main(String[] args) {
            System.out.println(tableSizeFor(9)); //9的二进制更能看出以下变化
        }

        static final int tableSizeFor(int cap) {
            System.out.println(Integer.toBinaryString(cap)); //1001
            int n = cap - 1;
            System.out.println(Integer.toBinaryString(n)); //1000
            n |= n >>> 1; //无符号右移,前面补0
            System.out.println(Integer.toBinaryString(n)); //右移再或,1100
            n |= n >>> 2;
            System.out.println(Integer.toBinaryString(n)); //再移动2位, 1111
            n |= n >>> 4;
            System.out.println(Integer.toBinaryString(n)); //就这么长,再迁移也是1111
            n |= n >>> 8;
            System.out.println(Integer.toBinaryString(n));
            n |= n >>> 16;
            System.out.println(Integer.toBinaryString(n)); //Integer的最大长度32位,16折半后迁移全覆盖
            System.out.println(Integer.toBinaryString(n + 1));
            return n + 1; //+1后变为 10000 ,也就是16 , 2的4次方
        }

4)总结:
map的构造函数没有你想象的那么简单!
无参构造时,容量=16,因子=0.75。第一次插入数据时,才会初始化table、阈值等信息有参构造时,不会容忍你胡来,会取大于但是最接近你容量的2的整数倍(想一下为什么?提示:和扩容规则有关)
无论哪种构造方式,扩容阈值最终都是 =(容量*因子)

2.3.3 HashMap插入方法
目标:图解+代码+断点分析put源码
1)先了解下流程图
在这里插入图片描述
2)关于key做hash值的计算
当我们调用put方法添加元素时,实际是调用了其内部的putVal方法,第一个参数需要对key求hash值

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

小提问:map里所谓的hash是直接用的key的hashCode方法吗?

  static final int hash(Object key) {
        int h;
        //【知识点】hash扰动
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在这里插入图片描述
结论:使用移位和异或做二次扰动,不是直接用的hashCode!

3)核心逻辑

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
       //onlyIfAbsent:true不更改现有值;evict:false表 示table为创建状态
       //tab=数组,p=插槽指针,n=tab的长度,i数组下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)//数组是否null或者==0, 第1次put为空
        //初始化数组(or扩容),所以table是在这里初始化的,不是new的时候! 
        //初始时,n=16
            n = (tab = resize()).length;
            //【知识点】为何1 与 17 在一个槽上!秘密就藏在寻址这里,后面重点讲
        if ((p = tab[i = (n - 1) & hash]) == null) //寻址:(n - 1) & hash(重要!)
        //当前插槽没有值,空的!将新 node直接扔进去
            tab[i] = newNode(hash, key, value, null);
        else {
        //有值,说明插槽上发生了碰撞,需要追加成链表了!
            Node<K,V> e; K k;
            //e=是否找到与当前key相同的节点,找到说明是更新,null说明是新key插入
            //k=临时变量,查找过程中的key在这里暂存用
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            //如果不是这个key,但是类型是一个红黑树节点 
//这说明当前插槽的链很长,已经变成红黑树了,就调putTreeVal,扔到这颗树上去 
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //如果都不是以上情况,那就是链表了
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                    //顺着链表一直往后跳,直到遍历到尾巴节点
                    //然后把key封装成 新node追加到尾巴上
                        p.next = newNode(hash, key, value, null);
                        //链表长度计数如果 >8转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//红黑树的创建,内部会判断是否大于 64,64以内先扩容
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))// 如果遍历过程中找到相同key,那就赋给e,break跳出for循 环,执行后面的逻辑
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
            // 如果e非空,说明前面一顿猛如虎的操作后,找到了相同的key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;// 看看onlyIfAbsent,如果让覆盖那就覆盖,不让那就 算了
                afterNodeAccess(e);
                return oldValue;;// 返回覆盖前的value值,也就是put方法的返回值
            }
        }
        ++modCount; //用来记录HashMap的修改次数
        if (++size > threshold) //key的数量是否大于阈值
            resize(); //如果size大于threshold,就需要进行扩容
        afterNodeInsertion(evict);
        return null;
    }

4)重点(寻址计算):
接上文,关于hash值取得后,放入tab的哪个插槽,也就是所谓的寻址我们重点来讲

(n - 1) & hash

我们还是以开始的例子,1和17为例,他们的hash计算后正好是1和17本身,我们可以验证一下

Integer i = new Integer(1); 
Integer j = new Integer(17); 
System.out.println(i.hashCode() ^ i.hashCode()>>16); //1 
System.out.println(j.hashCode() ^ j.hashCode()>>16); //17 

开始位运算

默认n=16,n-1也就是15,二进制是 1111 
那么 15 & 1 
1 1 1 1 
0 0 0 1 
与运算后 = 1 
再来看15 & 17,17是 10001 
1 1 1 1 
1 0 0 0 1
与运算后 = 1 
所以,1和17肯定会落在table的1号插槽上!两者会成为链表,解释了我们前面的案例 
原理:不管你算出的hash是多少,超出tab长度的高位被抹掉,低位是多少就是你所在的槽的位置,也就是 table的下标

思考:为什么不用mod(模运算)进行寻址?mod也能保证不会超出数组边界,岂不是更简单直观?

 public static void main(String[] args) {
        bit();
        mod();

    }


    public static void bit() {
        int num = 10000 * 10;
        int a = 1;
        long start = System.currentTimeMillis();
        for (int i = num; i > 0; i++) {
            a &= i;
//            a  = a&i;
        }
        long end = System.currentTimeMillis();
        System.out.println("BIT耗时>>" + (end - start));

    }

    public static void mod() {
        int num = 10000 * 10;
        int a = 1;
        long start = System.currentTimeMillis();
        for (int i = num; i > 0; i++) {
            a %= i;
//            a = a%i;
        }
        long end = System.currentTimeMillis();
        System.out.println("MOD耗时>>" + (end - start));

    }

跑一下试试?
结论:
一切为了性能

2.3.4 HashMap扩容方法

目标:图解+代码(map扩容与数据迁移)
注意:扩容复杂、绕、难
备注:在resize时会丢数据,线程不安全的
图解: 假设我们 new HashMap(8)
迁移前:长度8 扩容临界点6(8*0.75)
在这里插入图片描述
在这里插入图片描述
核心源码resize方法

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; //旧的数组先拿出来
        int oldCap = (oldTab == null) ? 0 : oldTab.length; ;//旧数组是null,那就是初 始化咯
        int oldThr = threshold; //扩容临界点(旧)
        int newCap, newThr = 0; //临时变量,数组容量(新)、扩容临界点(新)
        if (oldCap > 0) {
        // 扩容的时候调用
            if (oldCap >= MAXIMUM_CAPACITY) {//如果旧值达到上限
                threshold = Integer.MAX_VALUE; //扩容阈值也调到最大,从此再无意义
                //不扩了,直接返回旧的。上限了还扩什么扩
                return oldTab;
            }
            //如果没到上限就计算新容量,注意这时候还没发生实际的数组扩容,真正的扩容迁数据操作在下面 
//将旧容量左移1位,也就是乘以2作为新容量,所以map是每次扩到之前的2倍 
//链表是右移1位再加上旧长度,也就是扩为原来的1.5倍,注意区别 

            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
                 // 同时,阈值也乘以2,为下次扩容做准备
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
        // HashMap(int initialCapacity, float loadFactor)初始化的时候调用 
// 将cap和thres相等,约定 

            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        // HashMap() 初始化的时候调用,注意前面验证过了,是在第一次put的时候调的
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
        //如果新阈值为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; //以新容量为长度,创建新数组
        if (oldTab != null) { //如果旧数组不为空,说明有数据要迁移
            for (int j = 0; j < oldCap; ++j) { //遍历数组
                Node<K,V> e;  //临时变量,记录当前指向的node
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null; //gc处理
                    if (e.next == null)
                    //只一个节点,赋值到新数 组的索引下即可
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                    // 如果变成了树,拆成俩拼到新 table上去
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    //如果是链表,拆成两个(重点!!!)
                    //低位链表(原位置 i)
                        Node<K,V> loHead = null, loTail = null;
                        //高位链表(i+n位 置)
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//oldCap=8,旧容量为 100
                            // 如果为0,说明e的hash没超出旧cap长度去,在低位不动即 可
                                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;//下标:原位置+原数组长度 (重点!)
                            //这地方诠释了为什么map要两倍扩容,对应位置位运算后,加上原 长度就行了
                        }
                    }
                }
            }
        }
        return newTab; //返回新数组
    }

总结(扩容与迁移):
1、扩容就是将旧表的数据迁移到新表
2、迁移过去的值需要重新计算hashCode,也就是他的存储位置
3、关于位置可以这样理解:比如旧表的长度8、新表长度16
旧表位置4有6个数据,假如前三个hashCode是一样的,后面的三个hashCode是一样的
迁移的时候;就需要计算这6个值的存储位置
4、如何计算位置?采用低位链表和高位链表;如果位置4下面的数据e.hash & oldCap等于0,那么它对应的就是低位链表,也就是数据位置不变
5、 e.hash & oldCap不等于0呢?就要重写计算他的位置也就是j + oldCap,(4+8)= 12,就是高位链表位置(新数组12位置)

2.3.5 HashMap获取方法

目标:图解 (这个简单!)
获取流程
在这里插入图片描述
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) {
        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) {
            // 如果table对应的索引位置上有值
            if (first.hash == hash && // always check first node
            // 看下第一个元素的key是不是要查找的那个,是的话,返回即可
                ((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;
    }

总结:
查询思路比较简单,如果是数组直接返回、如果是红黑实例,就去树上去找,最后,去做链表循环查找

2.3.6 HashMap移除方

移除流程
在这里插入图片描述
tips:
两个移除方法,参数上的区别
走的同一个代码
移除方法:一个参数

  public V remove(Object key) {
        Node<K,V> e; /// 定义一个节点变量,用来存储要被删除的节点(键值对)
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

移除方法:二个参数

/**
* Implements Map.remove and related methods 
*
* @param hash 扰动后的hash值 
* @param key 要删除的键值对的key,要删除的键值对的value,该值是否作为删除的条件取决 
于matchValue是否为true 
* @param value key对应的值 
* @param matchValue 为true,则当key对应的值和equals(value)为true时才删除;否则不关心value的值 
* @param movable 删除后是否移动节点,如果为false,则不移动 
* @return 返回被删除的节点对象,如果没有删除任何节点则返回null 
*/ 
 final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //注意,p是当前插槽上的头 节点!
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //和查询一样,确保当前槽上不是空的
            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; // 如果key相同,说明找到了,将它赋给node
            else if ((e = p.next) != null) {
            //否则,沿着next一直查找
                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; //如果找到,赋值给node
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //如果node不为空,说明根据key匹配到了要删除的节点
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                                 ;// 红黑删除
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                    // 不是树,那如果 node == p 的意思是该node节点就是 首节点
                else if (node == p)
                    tab[index] = node.next; // 删掉头节点,第二个节点上位到数组槽上
                else
                    p.next = node.next;//如果不是,那就将p的后指针指向node的后指针,干 掉node即可
                ++modCount; //HashMap的修改次数递增
                --size; // HashMap的元素个数递减
                afterNodeRemoval(node);
                return node; //返回删除后的节点
            }
        }
        return null; //找不到删除的node,返回null
    }

总结:
移除和查询路线差不多,找到后直接remove
注意他的返回值,是删除的那个节点的值

拓展:
1)关于头插法与尾插法
jdk1.7 链表采用头插法,头插法比较快,但多线程下容易造成环形列表;(已过时,了解即可)
jdk1.8是尾插法;
无论哪种链表插入,线程依然是不安全的!没有从根本上解决并发问题
2)为什么说HashMap是线程不安全的
我们从前面的源码分析也能看出,它的元素增删改的时候,没有任何加锁或者cas操作。
而这里面各种++和–之类的操作,显然多线程下并不安全

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值