MAP学习

MAP

学习目标

  • Map是什么,有哪几种实现,Map总体架构是什么,每种实现有什么异同,每种实现的适用范围,优点和缺点,每种实现背后的思想;

  • 每种Map的数据结构,操作方法:添加,删除,查找主键和对应内容方法;

  • Map主键的考虑

  • 对于多线程读写的考虑

Java中的Map主要有哪几种?之间有什么区别。链接:链接:https://t.zsxq.com/j2fAQfu
Java中遍历Map的几种方式。链接:链接:https://t.zsxq.com/ujYFUfu
HashMap和HashTable有何不同?链接:链接:https://t.zsxq.com/e66QNr3
HashMap 和 ConcurrentHashMap 的区别?链接:链接:https://t.zsxq.com/Rb6uVVv
同样是线程安全的Map,HashTable和ConcurrentHashMap之间有什么区别?链接:链接:https://t.zsxq.com/q3Nnaqf
hashCode()和equals()方法的作用,二者有什么关系?链接:链接:https://t.zsxq.com/rRZBeAY
HashMap和TreeMap的区别是什么?链接:链接:https://t.zsxq.com/a23JYr7
ConcurrentHashMap和LinkedHashMap有什么区别?链接:链接:https://t.zsxq.com/UF6yrJA
所有的类都可以作为Map的key吗?有什么需要注意的吗?链接:链接:https://t.zsxq.com/vNJAmAI
什么是ConcurrentSkipListMap?他和ConcurrentHashMap有什么区别?链接:链接:https://t.zsxq.com/vB2NbuN

What: Map是什么,有哪些实现

Java中的Map主要有HashMap, HashTable, LinkedHashMap和ConcurrentHashMap几种常用的。

四种Map的区别

  • HashMap 是非线程安全的。
  • HashTable 是线程安全的。
  • LinkedHashMap 是 HashMap 的一个子类,它保留插入的顺序。
  • ConcurrentHashMap 也是线程安全的,在加锁粒度上比HashTable要细,性能会更好一些。

HashMap

wiki

  • java集合框架08——HashMap和源码分析
  • HashSet and HashMap
  • Class HashMap
  • Java 8系列之重新认识HashMap

HashMap概述

HashMap的API

void                 clear()
Object               clone()
boolean              containsKey(Object key)
boolean              containsValue(Object value)
Set>     entrySet()
V                    get(Object key)
boolean              isEmpty()
Set               keySet()
V                    put(K key, V value)
void                 putAll(Map map)
V                    remove(Object key)
int                  size()
Collection        values()

HashMap的数据结构

HashMap的存储结构是一个Node数组,存放Node实体;

transient Node[] table;

Node的定义

// 
static class Node implements Map.Entry {
    final int hash;
    final K key;
    V value;
    Node next;

    // 一个hash节点包括hash值,键值k,值v和下一个节点
    Node(int hash, K key, V value, Node 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;
    }

    // 判断两个Entry相等的条件:key和value都相等。
    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的底层主要是基于数组和链表实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储位置的。HashMap中主要是通过key的hashCode来计算hash值,然后通过hash值选择不同的数组来存储。只要hashCode相同,计算出来的hash值就一样,如果存储对象多了,就有可能不同的对象计算出来的hash值是相同的,这就出现了所谓的hash冲突,解决hash冲突的方法很多,具体可以参见我这篇博客:数据结构与算法07 指哈希表。HashMap的底层是通过链表来解决hash冲突的。

图片

图中紫色部分代表哈希表,其实就是哈希数组,数组的每个元素都是一个单链表的头结点,链表是用来解决hash冲突的,如果不同的key映射到了数组的同一位置,那么就将其放入单链表中。下面的图可能在代码的角度更能说明问题:

图片

源码分析(基于jdk1.8)

关键的变量

//默认初始容量是16,必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 
//最大容量(必须是2的幂且小于2的30次方,传入容量过大会被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
 
//默认加载因子,所谓加载因子是指哈希表在其容量自动增加之前可以达到多满的一种尺度
static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
//存储Entry的默认空数组
static final Entry[] EMPTY_TABLE = {};
 
//存储Entry的数组,长度为2的幂。HashMap采用拉链法实现的,每个Entry的本质是个单向链表
transient Entry[] table = (Entry[]) EMPTY_TABLE;
 
//HashMap的大小,即HashMap存储的键值对数量
transient int size;
 
//HashMap的阈值,用于判断是否需要调整HashMap的容量
int threshold;
 
//加载因子实际大小
final float loadFactor;
 
//HashMap被修改的次数,用于fail-fast机制
transient int modCount;

我们主要来看看loadFactor属性,loadFactor表示Hash表中元素的填满程度。

若加载因子设置过大,则填满的元素越多,无疑空间利用率变高了,但是冲突的机会增加了,冲突的越多,链表就会变得越长,那么查找效率就会变得更低;

若加载因子设置过小,则填满的元素越少,那么空间利用率变低了,表中数据将变得更加稀疏,但是冲突的机会减小了,这样链表就不会太长,查找效率变得更高。


构造方法

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);
}

从上述代码可以看出HashMap主要设置loadFactorthreshold两个变量

  • loadFactor的设置:如果不传,设为默认值 0.75f;

  • threshold的设置

    • Java8 HashMap之tableSizeFor
    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;
    }
    

    tableSizeFor的作用是返回大于输入参数且最近的2的整数次幂的,比如输入10,返回16;

    对于n = 0001******,大于n并且最近的2的整数次幂的数为n1 = 0010******

    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    

    这部分的代码的作用是将n第一位为1的bit和之后的bit都变为1,比如n = 0001******,经过上述操作后,n = 0001111111;

    n+1之后n = 0010000000,得到大于n并且最近的2的整数次幂的数;

    int n = cap - 1;的作用是针对2的整数次幂,比如n = 8 = 001000,如果不执行减1,则得到16。


HashMap中的红黑树

  • 教你初步了解红黑树
  • HashMap.comparableClassFor(Object x)方法解读
  • JDK8:HashMap源码解析:comparableClassFor、compareComparables、tieBreakOrder方法
  • HashMap 在 JDK 1.8 后新增的红黑树结构

使用红黑树的原因是查找时间复杂度为O(lgn).

红黑树的五条性质

  1. 每个结点要么是红的,要么是黑的。
  2. 根结点是黑的。
  3. 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
  4. 如果一个结点是红的,那么它的俩个儿子都是黑的。
  5. 对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。

红黑树重点关注点

  • 生成红黑树

  • 插入节点

  • 删除节点

treeify:将链表转化为红黑树
final void treeify(Node[] tab) {
    TreeNode root = null;
    for (TreeNode x = this, next; x != null; x = next) {
        next = (TreeNode)x.next;
        x.left = x.right = null;
        if (root == null) { // 初始化数的根节点
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class kc = null;
            for (TreeNode p = root;;) {
                int dir, ph;
                K pk = p.key;
                // 比较插入元素和当前元素hash值大小,当前元素初始为root;
                // dir表示比较的结果
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                // 针对hash值相等的情况进行判断
                // 首先通过comparableClassFor方法判断key是否定义了compare方法;
                // 然后通过compare方法比较大小
                // 如果都相等,调用tieBreakOrder方法比较大小
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode xp = p;
                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;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}
balanceInsertion方法:新增元素后平衡红黑树

红黑树插入节点后需要恢复红黑树的性质,首先考虑父节点是祖父节点的右孩子还是左孩子;

如果父节点是祖父节点的左孩子,需要分为下面三种情况

  • 插入修复情况1:如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色;

    将当前结点的父结点和叔叔结点涂黑,祖父结点涂红,把当前结点指向祖父结点,从新的当前结点重新开始算法。

  • 插入修复情况2:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子

    当前结点的父结点做为新的当前结点,以新当前结点为支点左旋。然后就变成情况3,通过对情况3的修复,可以修复情况2.

  • 插入修复情况3:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子

    父结点变为黑色,祖父结点变为红色,在祖父结点为支点右旋,然后父节点变为当前节点。

如果父节点是祖父节点的右孩子,和上述情况类似,只不过对于情况3,进行右旋后,可以变为情况2。

static  TreeNode balanceInsertion(TreeNode root,
                                                    TreeNode x) {
    x.red = true;
    for (TreeNode xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 需要考虑当前节点的父节点是祖父节点的左孩子还是右孩子
        if (xp == (xppl = xpp.left)) {
            // 修复情况1:当前结点的父结点是红色,并且祖父结点的另一个子结点(叔叔结点)是红色
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                // 修复情况2:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子
                if (x == xp.right) {
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 修复情况3:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子
                if (xp != null) {
                    // 父结点变为黑色
                    xp.red = false;
                    if (xpp != null) {
                        // 祖父结点变为红色
                        xpp.red = true;
                        // 在祖父结点为支点右旋
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        else {
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

putTreeVal()方法
  • 比较插入节点和当前节点p的大小;

    寻找插入位置的方法和treeify类似,通过比较hash值的大小确定位置,但是对于hash值相同的情况有所不同。

    如果当前节点和插入节点的hash值相同,当前节点的左孩子和右孩子会分别调用find()方法,查找以左孩子或者右孩子为root节点的子树中是否有和插入节点相同的节点,如果有则返回此节点,否则调用tieBreakOrder得出比较结果。

    代码中使用searched表示是否进行过上述的搜索,这个循环中只会进行一次搜索。

  • 通过比较大小的结果,更新当前节点为p的左孩子或者右孩子。如果左孩子或右孩子为null,将插入节点插入到红黑树中,然后调用balanceInsertion修复红黑树的性质。

final TreeNode putTreeVal(HashMap map, Node[] tab,
                               int h, K k, V v) {
    Class kc = null;
    boolean searched = false;
    TreeNode root = (parent != null) ? root() : this;
    for (TreeNode p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode q, ch;
                searched = true;
                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;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node xpn = xp.next;
            TreeNode x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

rotateLeft:左旋和rotateRight:右旋

rotateLeft方法的输入参数为root和将进行左旋的节点p,左旋的意义在于将节点p变为其右孩子的左孩子。

方法中使用r表示节点p的右孩子,pl表示节点p的左孩子,zhu

  • 将节点r的左孩子rl变为节点p的右孩子(rl的值比p大);
  • 将r的父节点更换为p的父节点
    • 如果p为root节点,则将r变为root节点;
  • 将p作为r的左孩子;
static  TreeNode rotateLeft(TreeNode root,
                                              TreeNode p) {
    TreeNode r, pp, rl;
    if (p != null && (r = p.right) != null) {
        // 将节点r的左孩子rl变为节点p的右孩子;
        if ((rl = p.right = r.left) != null)
            rl.parent = p;
        // 将r的父节点更换为p的父节点
        // 如果p为root节点,则将r变为root节点;
        if ((pp = r.parent = p.parent) == null)
            (root = r).red = false;
        // 需要判断p是父节点的左孩子还是右孩子
        else if (pp.left == p)
            pp.left = r;
        else
            pp.right = r;
        // 将p作为r的左孩子
        r.left = p;
        p.parent = r;
    }
    return root;
}

rotateRight方法的输入参数为root和将进行右旋的节点p,右旋的作用在于将节点p变为其左孩子的右孩子。

方法中使用l表示节点p的右孩子,pl表示节点p的左孩子

  • 将节点l的右孩子lr变为节点p的左孩子(lr的值比p小);
  • 将l的父节点更换为p的父节点
    • 如果p为root节点,则将l变为root节点;
  • 将p作为l的右孩子;
static  TreeNode rotateRight(TreeNode root,
                                               TreeNode p) {
    TreeNode l, pp, lr;
    if (p != null && (l = p.left) != null) {
        // 将节点l的右孩子lr变为节点p的左孩子
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
        // 将l的父节点更换为p的父节点
        // 如果p为root节点,则将l变为root节点
        if ((pp = l.parent = p.parent) == null)
            (root = l).red = false;
        else if (pp.right == p)
            pp.right = l;
        else
            pp.left = l;
        // 将p作为l的右孩子
        l.right = p;
        p.parent = l;
    }
    return root;
}

区分左旋和右旋的方法:

  • 对某一节点P进行左旋和右旋,作用都是将节点变为孩子节点的左孩子或者右孩子;
  • 如果将节点P变为做孩子节点的左孩子,则要求P的值比孩子节点的值小,所以是将节点P变为右孩子节点的左孩子;
moveRootToFront方法

在将链表转化为红黑树后,Root节点可能不是tab中对应索引的元素,通过转换可以将tab对应索引的元素设置为Root。

static  void moveRootToFront(Node[] tab, TreeNode root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;
        TreeNode first = (TreeNode)tab[index];
        if (root != first) {
            Node rn;
            tab[index] = root; // 将tab对应索引的元素设置为Root。
            TreeNode rp = root.prev;
            if ((rn = root.next) != null)
                ((TreeNode)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}

hash的计算:hash()方法:

  • JDK 源码中 HashMap 的 hash 方法原理是什么?
  • 浅谈HashMap中的hash算法
  • Java Object.hashCode()返回的是对象内存地址?
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

图片

计算hash值可以分为两部分

  1. 高位运算:h^(h >>> 16):右移16位,正好是32位的一半,高半区和低半区做异或,是为了混合原始哈希码的高位和低位,加大低位的随机性。

  2. 取模运算:(n-1) & hash:可以解释HashMap的数组长度是2的整数幂

    首先,h & (length-1)相当于h % length,但是h % length效率比较低(HashTable中是这儿干的)。为啥h & (length-1)相当于h % length呢?现在假设length为2的幂,那么length就可以表示成100......00的形式(表示至少1个0),那么length-1就是01111....11。对于任意小于length的数h来说,与01111...11做&后都是h本身,对于h=length来说,&后结果为0,对于大于length的数h,&过后相当于h-j*length,也就是h % length。容量为2的整数幂,方便做&运算,效率高。

    其次,length为2的次幂的话,是偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h & (length-1)的最后一位可能为0也可能为1(取决于h的值),即结果可能为奇数,也可能为偶数,这样便可以保证散列的均匀性,即均匀分布在数组table中;而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h & (length-1)的最后一位肯定为0,级只能为偶数,这样任何hash值都会被映射到数组的偶数下标位置上,这便浪费了近一半的空间!因此,length去2的整数次幂,也是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀的散列。


扩容方法:resize()方法

final Node[] resize() {
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) { // 如果扩容前数组大小以及达到最大值,修改阈值为为Int的最大值,则以后不会再扩容。
            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;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (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[] newTab = (Node[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null; // 释放旧的Node列表中的引用
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            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;
                    }
                }
            }
        }
    }
    return newTab;
}

resize()方法可以分为两部分

  • 计算newCapnewThr:newCapnewThr都是oldCapodlThr乘以2进行扩大;
  • 将旧的数组中的元素迁移到新的数组中
    • 如果Node是单节点,计算位置e.hash & (newCap - 1);
    • 如果Node是TreeNode,执行split()方法;
    • 如果Node是单链表,next不为null,通过(e.hash & oldCap) == 0将链表中的节点分为两部分,(e.hash & oldCap) == 0的节点放在当前位置(newTab[j] = loHead),(e.hash & oldCap) == 1的节点放在当前节点+oldCap处(newTab[j + oldCap] = hiHead

jdk1.8中计算元素在新数组位置的方法和jdk1.7中相同,实现方法不同。可以参考wiki:[Java 8系列之重新认识HashMap](


插入,删除,查找,修改

插入方法
  • 插入单个节点
  • 插入一个Map
put方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node 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)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;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

首先判断table是否为null或者是否长度为0,如果table为null或者长度为0,则进行扩容;

然后计算key的hash值并与数组长度做取模运算,获得新的节点在table中的索引;

判断table中索引节点的情况

  • 索引处节点和新的节点相同,不做处理;
  • 如果不相同并且索引节点是TreeNode,说明这个索引对应的元素用红黑树存储,然后调用putTreeVal插入元素;
  • 如果不相同并且索引节点是Node,将新的节点放在链表的尾部,同时判断链表的长度是否大于等于TREEIFY_THRESHOLD,如果大于等于,调用treeifyBin将链表树形化转化为红黑树。

https://tech.meituan.com/java_hashmap.html)


treeifyBin()方法

treeifyBin()的主要作用是将链表中的元素全部转化为红黑树的节点TreeNode,并且按照顺序排列,最后调用TreeNode类的方法treeify()将链表转化为红黑树。

final void treeifyBin(Node[] tab, int hash) {
    int n, index; Node e;
    // 判断是需要扩容或者树形化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode hd = null, tl = null;
        do { // 将每个节点都转化为红黑树的节点,按照顺序连接
            TreeNode p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

查找方法
查找单个节点
  • 通过计算hash值,获取tab中的位置处的元素first;
  • 首先比较first和查找元素,如果不相同,接着判断使用链表还是红黑树,使用相应的方法查找元素。
public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node getNode(int hash, Object key) {
    Node[] tab; Node 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)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;
}
keyset

你可能感兴趣的:(MAP学习)