栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

HashMap类

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

HashMap类

一、简介 1.存储结构及特点

HashMap 底层的数据结构:
底层是利用**邻接表(散列表)**的形式进行存储的:数组 + 链表/红黑树。

HashMap 中每个节点 的存储方式为形式,
其中,key 无序不可重复,value可以重复。

2.存储结构的由来

首先我们需要明白:

· 当hash值不同时,对象一定不同。

原因:因为同样的计算方法计算同样的东西,计算结果一定相同。结果不同,就说明他俩根本不是同一个东西。

· 当hash值相同时,对象可能相同,也可能不同。

原因:因为不同的东西经过相同的计算方式,结果可能会相同。

所以,此时还需要借助equals()方法进行内容的具体判断。

也就是说,用hashCode()方法计算对象的存储位置时,不同的对象是可以产生相同的hash值的。
因此,当同一个hash值对应的对象不止有一个时,就将相同地址的对象用链表结构连接起来。

3.邻接表的存储方式

HashMap中底层的数组部分,是可以通过key计算出来的hash值直接进行快速访问的。

**数组内存储的内容是一个Entry类,**这个类有三个组成部分:key+value(键值对),next(指向下一个Entry类对象的指针)。

后面链表中的每个节点,存的也是一个 由 key+value(键值对),以及指向下一个节点地址的next指针包装成的一个Entry对象。

4.HashMap的存储过程

在往HashMap中put节点(键值对)时,首先通过底层重写的散列算法hashCode()方法计算出key的hash值,从而确定要将当前节点插入到数组中的哪个位置。要将当前节点定位到散列表的相应位置,所以需要重写hashcode()方法。

但是,当两个key计算出来的hash值相同时,则后来的元素就要添加到先来的元素的后面(JDK1.8采用尾插法),从而形成链表。所以,同一个链表上的Hash值是相同的(在jdk1.8中,如果链表的长度>8,则此链表就会转化成红黑树,从而提高查找效率)。然后依次遍历该位置后面链表中的所有元素,此时就需要调用该类重写的equals()方法进行内容的具体比较了。相等则修改key对应的value值,不等则添加该节点。

实际上,HashMap中判断元素是否重复是有两个过程的,首先生成即将插入的新对象其key的hash值。当我们发现没有相同的散列值时,就可以判断这是一个不可能重复的元素,就可以直接插入了。当我们发现table中已经有相同的散列值时,并不能直接判断此对象就是个重复元素(因为不同的对象也可能产生相同的hash值),此时还需要调用equals()方法对具体内容再判断一次,如果判断的结果还是真,就表示这真的是个重复元素,就不做存入处理,只做值value的更新处理;如果equals判断为假,那么就重新比较其他的节点,再进行插入判断。

5.HashMap中,需要重写equals()和hashCode()的原因

Object中的原生方法:

  • hashCode() :返回的是对象的地址引用。

    所以,在这种情况下,不同对象的hash值肯定不同,即使两个对象的内容完全一致,但由于地址引用不同,所以hash值也就不同。

  • equals() :底层默认的比较方式是"=="号。

    所以,对于引用数据类型来说,比较的是对象的地址(8个基本数据类型+String类型进行值比较)。

equals()方法和hashCode()方法都属于Object类,在Java中,所有的类都是Object类的子类,也就是说,任何Java对象都可调用Object类的方法。

很明显,equals()方法就是用来判断,两个对象是否是同一个地址引用的。在Object类源码中,其底层是使用了“==”来实现比较的。也就是说,通过比较两个对象的内存地址是否相同,来判断这两个对象是否是同一个对象。但实际上,该equals()方法通常没有什么太大的实用价值。我们往往需要用equals()方法来判断, 2个对象在逻辑上是否等价,而非验证它地址内存的唯一性。这样我们在实现一些自己的类时,就需要重写equals()方法。

而在HashMap中,需要通过重写hashCode()方法,重新定义计算key的hash值的计算方式(根据key的实际内容进行计算,而不是返回对象的地址引用)。由于内容不同的对象也可能被计算出相同的hash值,所以此时就需要通过重写equals()方法进行对象的内容判断(内容相同的对象被认为是同一个对象,返回true。而不是equals()利用原来的"=="号,来比较引用数据类型的地址是否相同。)。

6.JDK1.7和1.8的区别 1>.存储结构:

JDK1.7中,底层的存储结构是:数组+链表。

而JDK1.8中:数组+链表/红黑树(链表长度>8时,会由链表转换为红黑树以提高效率)。

思考:

红黑树是为了解决链表查询出现O(n)情况,那么为什么不用其他树呢?

如平衡二叉树等,我们通过以下二方面分析:

​ 平均插入效率:链表>红黑树>平衡二叉树

​ 平均查询效率:平衡二叉树>红黑树>链表

可以看出红黑树介于二者之间,hashMap作为各种操作频繁的容器,自然选择综合性能较好的红黑树

2>.新节点插入链表的方式:

JDK1.7中用的是头插法(扩容后与原位置相反,因为resize()会导致环形链表)。

JDK1.8用的是尾插法(因为1.8中引入了红黑树结构,要通过遍历链表,将链表转换为红黑树),扩容后位置与原链表相同。

3>.hash算法简化:

Hash值的计算区别

JDK1.7:h^ =(h>>>20)^(h>>>12) return h ^(h>>>7) ^(h>>>4);

JDK1.8:(key==null)?0:(h=key.hashCode())^(h>>>16);

JDK1.7中因为要保持hash函数的散列性,所以进行了多次的异或等位运算。

JDK1.8因为链表长度超过8会转为红黑树,所以我们可以稍微减少元素的散列性,从而避免多次进行异或等位运算。

4>.扩容机制:

JDK1.7扩容条件:元素个数 > 容量(16) * 加载因子 (0.75) && 插入的数组位置有元素存在。
JDK1.8扩容条件 :元素个数 > 容量 (16) * 加载因子(0.75)。

虽然都是进行2倍扩容,但是JDK1.7在扩容的时候,会重新计算位置。

JDk1.8则不会,只要关注原hash值新增的那个bit位是1还是0就好了。

是0的话,扩容前后索引不变,新索引 = 原索引。

是1的话,新索引 = 原索引+ oldCap(旧数组长度)。

7.方法简介 1>.基本方法 1.增加( put(key,value) ) 2.删除( remove(key) ) 3.更改(replace(key,value))

(其实,先put(key,value1),后又put(key,value2),value2覆盖了value1,就是发生了更改。)

4.查找(get(key)) 2>.重要方法 1.keySet()方法:

可以通过 keySet()方法,获取 HashMap中所有的key,此方法的返回值是Set类型。
例:Set keys = hashMap.keySet();

2.entrySet()方法:

可以通过 entrySet()方法,获取HashMap中所有的Entry对象,每个Entry对象的信息包括(key + value + nextNode)。
例:Set< Map.Entry > entrys = hashMap.entrySet()

3>.特殊方法 1.getOrDefault(key , defaultValue)方法:

如果key存在,就返回其对应的value值;

如果key不存在,就返回默认值defaultValue(以前返回的是null)。

2.putIfAbsent(key , value)方法:

当key不存在时,向HashMap中添加此键值对;

如果此key存在,则不发生任何更改(以前用put()方法存放时,value值会发生改变)。

二、基础信息 1.继承的父类&实现的接口
public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
2.重要属性
//允许序列化和反序列化
    private static final long serialVersionUID = 362498820763181265L;

    
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    
    static final int MAXIMUM_CAPACITY = 1 << 30;

    
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    
    static final int TREEIFY_THRESHOLD = 8;

    
    static final int UNTREEIFY_THRESHOLD = 6;

    
    static final int MIN_TREEIFY_CAPACITY = 64;


    transient Node[] table;

    
    transient Set> entrySet;

    
    transient int size;

    
    transient int modCount;

    
    int threshold;

    
    final float loadFactor;
3.Node节点类
 
    static class Node implements Map.Entry {
        final int hash;//此节点的hash值
        final K key;//此节点的 键key
        V value;    //此节点的 值value
        Node next;//下一个节点

        //有参构造函数
        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; }

        //计算当前节点hash值的hashCode()方法,重写自父类Object。
        //(借助父类Object 本地native修饰的hashCode()方法进行计算)
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        //修改当前节点的 value值,并将原来的旧值返回
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        
        public final boolean equals(Object o) {
            //由于是引用数据类型,所以 "==" 比较的是变量的地址引用
            //如果 参数o 和 当前节点对象 的地址相同,表示是同一个对象
            if (o == this)
                return true;

            //首先判断 参数o的数据类型,如果数据类型和当前节点的数据类型相同
            if (o instanceof Map.Entry) {
                //将 形参o 类型转换为e
                Map.Entry e = (Map.Entry)o;

                //如果当前的 key 和 形参e.getKey()的地址相同,
                //并且,当前的 value 和 形参e.getValue()的地址相同,则认为是同一个对象
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
三、重要方法 1.构造函数

注意: 3个有参构造方法会给扩容阈值threshold赋值,而无参构造方法不会。

1>.无参构造
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

2>.有参构造 1.HashMap(int initialCapacity, float loadFactor)方法:
 //构造一个空的HashMap,具有指定的初始容量和负载因子。
    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);

        //如果参数合法,分别给 负载因子loadFactor 和 扩容阈值threshold 赋值。
        this.loadFactor = loadFactor;
        //注意 扩容阈值threshold, 需要通过tableSizeFor()方法计算后给出(因为表的长度必须是2的整数幂)
        this.threshold = tableSizeFor(initialCapacity);
    }


 
    static final int tableSizeFor(int cap) {//例:cap = 10
        int n = cap - 1;//n = 10-1 = 9 = 1001
        n |= n >>> 1;//n = 1001 | 0100 = 1101
        n |= n >>> 2;//n = 1101 | 0011 = 1111
        n |= n >>> 4;//n = 1111 | 0000 = 1111
        n |= n >>> 8;//n = 1111 | 0000 = 1111
        n |= n >>> 16;//n = 1111 = 15

        //n = 15 时,执行return n+1,最后返回10
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
2.HashMap(int initialCapacity)方法:
	//构造一个空的HashMap,具有指定的初始容量和默认负载因子(0.75)。
    public HashMap(int initialCapacity) {
        //只传入初始容量时,第二个负载系数将传入默认值DEFAULT_LOAD_FACTOR = 0.75
        //然后利用上面的构造函数HashMap(int initialCapacity, float loadFactor)进行构造
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
3>.利用Map构造HashMap

HashMap(Map m) 方法:

    public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }


final void putMapEntries(Map m, boolean evict) {
        int s = m.size();//记录形参m内的元素个数

        if (s > 0) {
            //如果 table 没有被初始化过
            if (table == null) { // pre-size
                //由于 扩容阈值threshold = capacity(表长) * loadFactor(负载系数)
                //所以先用ft大概算一下所需表长
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果 计算得到的所需表长t > 扩容阈值
                if (t > threshold)
                    //则更新扩容阈值,通过tableSizeFor(t)得到一个不小于t的2的整数幂。
                    threshold = tableSizeFor(t);
            }
            //如果已初始化过,并且m的元素个数>扩容阈值
            else if (s > threshold)
                //则进行扩容处理
                resize();

            //复制数据,将m中的所有元素添加到hashMap中
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }
2.增加put(K key, V value)方法
    //增加:存入 键值对型 节点
    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) {

        //tab:用于存储当前散列表table的地址引用
        //p : 用于表示当前散列表中的节点元素
        //n : 用于表示散列表数组的长度
        //i : 用于表示计算后的最终下标。
        Node[] tab; Node p; int n, i;

        //如果 table为null,或者长度为0
        if ((tab = table) == null || (n = tab.length) == 0)
            //为哈希表做初始化处理
            //先为table扩容(扩容就是做了初始化),并用n记录扩容后表的长度
            n = (tab = resize()).length;

        //如果 利用表长n和hash值计算出来的下标i,在哈希表中对应的存储单元为null
        if ((p = tab[i = (n - 1) & hash]) == null)
            //就将此节点放到表table的对应位置
            tab[i] = newNode(hash, key, value, null);

        //否则表示,此hash值对应的节点不止一个
        else {
            //e : 用于记录与待插节点的hash值和key都完全一致的节点。
            Node e; K k;
            //如果,数组中的节点p与新加节点的hash值相等
            //并且,键key也相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //此时表示已经找到了重复元素,用e记录旧节点的地址引用,后续需要进行值替换
                e = p;

            //如果新加节点的key与数组中节点的key不同,
            //第一种可能是,去后面的树中去找(涉及红黑树)
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeval(this, tab, hash, key, value);
            //如果不去树中查询,就去链表中遍历。(至此已知,链表的头元素与待插元素的key不一致)
            else {
                //开始循环遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //如果已经到达链表尾部,则表示此hash值对应的链表中没有与待插元素相同的key
                    if ((e = p.next) == null) {
                        //就把新节点添加到此链表的尾部,作为尾节点。
                        p.next = newNode(hash, key, value, null);
                        //如果添加完新节点后的链表长度>8,则由链表转换成红黑树。
                        // 为什么是长度>8?因为触发下面if语句的条件是:binCount >= 7。
                        // 由于binCount的初始值是0,
                        // 当binCount=7时,表示现在正在执行的是循环的第8次,
                        // 而且刚才又在链表的尾部添加了一个新节点,故此时链表长度是9,而9>8。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//将链表结构进行树化
                        break;
                    }
                    //如果在遍历链表的过程中,遇到与待插节点的hash值和key都相同的节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//p = e = p.next,循环继续往后走的条件
                }
            }

            //若 e != null,则表示此hashMap中,存在与待插元素同hash且同key的重复元素,
            // 需执行value的更新操作。
            if (e != null) { // existing mapping for key
                V oldValue = e.value;//保存旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//更新value值
                afterNodeAccess(e);
                return oldValue;//并将旧值返回
            }
        }

        ++modCount;
        //如果 节点个数size > 扩容阈值threshold,则需执行扩容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
3.删除remove() 1>. remove(Object key, Object value)方法:
	//删除节点:当key和value同时比对成功,才执行删除操作,否则不删。
    @Override
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }
2>. remove(Object key, Object value)方法:
	//删除:根据键key ,删除对应的节点对象
    public V remove(Object key) {
        Node e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }


	//删除节点操作的具体实现
    final Node removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        //tab:保存当前table数组的地址引用
        //n : 记录当前table数组的长度
        Node[] tab; Node p; int n, index;
        //如果 此hash值对应的桶中存在节点p
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node:用于记录查找结果
            Node node = null, e; K k; V v;
            //如果 传入的参数与桶中头节点的hash值和key比对成功。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;//用node记录已经找到的节点
            //如果 此桶中头节点的后面还有节点。
            else if ((e = p.next) != null) {
                //如果 头节点的后面是树结构
                if (p instanceof TreeNode)
                    //则 在树中返回查询结果
                    node = ((TreeNode)p).getTreeNode(hash, key);
                //否则表示,头节点后面是链表结构
                else {
                    //利用循环向后遍历链表中的节点,挨个比对它们的hash值和key
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //至此,已经用node记录了最终的查找结果
            //若 node != null,则表示找到了此key对应的节点
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //如果 这是个树节点
                if (node instanceof TreeNode)
                    //则在树中删除
                    ((TreeNode)node).removeTreeNode(this, tab, movable);
                //如果 这是桶中的那个头节点
                else if (node == p)
                    //则把头节点的后继节点更新为新的头节点
                    tab[index] = node.next;
                //如果 这是链表中的一个普通节点
                else
                    p.next = node.next;//将当前node节点从链表中移除
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
4.查找get(Object key)方法
	//查找:根据 键key,返回 值value
    public V get(Object key) {
        Node e;
        //getNode()方法:根据 key的hash值 和 键key,返回对应Node节点
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

   
    //根据 key的hash值 和 键key,返回对应Node节点
    final Node getNode(int hash, Object key) {
        //tab:保存当前数组的地址引用
        //first:某个桶中的头节点
        //n :记录table数组的长度
        Node[] tab; Node first, e; int n; K k;
        //如果 数组表table此hash值对应的桶中有节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果 传入的参数与数组对应桶中,头节点的hash值相等,key也相同
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                //表示此hash桶中的头节点,就是我们要找的节点
                return first;
            //如果 此hash值对应桶中的节点不只是这一个头节点
            if ((e = first.next) != null) {
                //如果 头节点后面连的是个树
                if (first instanceof TreeNode)
                    //在树中返回节点
                    return ((TreeNode)first).getTreeNode(hash, key);

                //否则表示,此hash值对应桶中的节点后面是个链表结构
                //则依次向后遍历每个节点,比较其hash值和key
                do {
                    //如果hash值和key比对成功
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;//则返回此节点
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
5.修改replace() 1>.replace(K key, V value)方法:
 	//修改:把key的值更新为value
    @Override
    public V replace(K key, V value) {
        Node e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;//修改value
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }
2>.replace(K key, V oldValue, V newValue)方法:
	//修改:当传入的key和oldValue都比对成功后,才将值更新为newValue
    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        Node e; V v;
        if ((e = getNode(hash(key), key)) != null &&
            ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }
四、核心方法 1.hash(Object key)方法:
 	
    //经过计算,返回 key的hash值
    static final int hash(Object key) {
        int h;
        //如果key!=null,借助hashCode()方法,计算key的hash值。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
2.resize()方法:
     
    final Node[] resize() {
        Node[] oldTab = table;//oldTab:用于保存扩容前旧表的地址引用
        //oldCap:用于记录扩容前table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//扩容之前的扩容阈值
        //newCap:扩容之后的table数组的长度
        //newThr:扩容之后,下次再触发的扩容阈值
        int newCap, newThr = 0;
        //如果旧表被初始化过,表示这是一次正常的扩容
        if (oldCap > 0) {
            //如果旧表数组长度已经超过最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                //则不再继续扩充,且设置下次扩容条件为 threshold = Integer.MAX_VALUE.
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果旧表数组长度没超过最大容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //则新数组长度扩容为旧数组长度的2倍.
                newThr = oldThr << 1; // double threshold
        }

        //此时 oldCap == 0,表示hashMap中的旧数组还没有被初始化过.
        //如果 旧的扩容阈值oldThr > 0.
        //注意:3个有参构造方法会给扩容阈值threshold赋值,无参构造方法不会
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        //此时 oldCap == 0,表示hashMap中的旧数组还没有被初始化过.
        //如果 旧的扩容阈值oldThr == 0.
        //则表示之前的hashMap是通过无参构造函数创建的,扩容阈值没有被赋过值.
        else {               // zero initial threshold signifies using defaults
            //给 新数组长度newCap 赋默认值16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //给 新的扩容阈值newThr 赋默认值0.75*16 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        //如果 新扩容阈值newThr == 0
        if (newThr == 0) {
            //就计算一个 新扩容阈值newThr
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

        threshold = newThr;//把 新的扩容阈值 赋给 全局变量
        //至此,计算出了 扩容后新数组的长度newCap 和 下次触发扩容函数的新阈值newThr
        //接下来,开始进行扩容操作.

       @SuppressWarnings({"rawtypes","unchecked"})
            Node[] newTab = (Node[])new Node[newCap];//创建了一个长度更长的新数组.
        table = newTab;//将扩容后的数组newTab的地址引用返回给table

        //如果 oldTab != null,则表示旧数组被初始化过(即旧表中有数据).
        if (oldTab != null) {

            //通过循环挨个遍历旧桶,并把旧桶内的元素移动到新桶中.
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                //如果旧数组中的某个桶内有数据(单个节点,链表或红黑树都有可能)
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;//资源回收

                    //如果当前桶内只有一个节点,无后继节点.
                    if (e.next == null)
                        //根据新数组长度重新计算索引,然后将此节点元素e存入到新表newTab中.
                        newTab[e.hash & (newCap - 1)] = e;

                    //如果当前桶内的节点是个树节点,则表示后面是个红黑树.
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    //如果后面是个链表结构
                    else { // preserve order
                        //低位链表:存放数组扩容前后,数组索引值未发生变化的节点.
                        //索引值不发生变化:新索引 = 旧索引。
                        //loHead作为低位链表的表头,loTail用来做链表的动态拼接.
                        Node loHead = null, loTail = null;
                        //高位链表:存放数组扩容前后,数组索引值会发生变化的节点.
                        //索引值发生变化:新索引 = 旧索引 + 旧表长度。
                        //hiHead作为高位链表的表头,hiTail用来做链表的动态拼接.
                        Node hiHead = null, hiTail = null;

                        Node next;
                        do {
                            next = e.next;
                            //若 e.hash = 15 = 0 1111 ,oldCap = 16 = 1 0000
                            //则 e.hash & oldCap = 0 1111 & 1 0000  = 0 0000 = 0
                            //表示 扩容前后,索引值不变:新索引 = 旧索引.
                            //此节点是个低位链表中的节点.
                            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;//用于标记尾节点,给尾节点的后继节点赋成null.
                            //由于扩容前后,低位链表内节点的索引值不需要发生改变.
                            //所以直接通过低位链表的表头,把整个链表添加到新数组的对应桶中.
                            newTab[j] = loHead;
                        }
                        //如果高位链表中有数据
                        if (hiTail != null) {
                            hiTail.next = null;//用于标记尾节点,给尾节点的后继节点赋成null.
                            //由于扩容前后,高位链表内节点的索引值会发生改变.
                            //所以通过高位链表的表头,把整个链表添加到新数组的对应桶中.
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
3.扩容机制


解析:

当桶内节点个数不止一个,且后面的结构是链表时,其扩容图示如上:

以索引值为15的桶为例,在图示中有5个节点元素。

我们已知,索引的计算方式 index = (key.hash) &(table.length - 1).

当扩容前的数组长度为16时,table.length - 1 = 16 - 1 = 15 = 1111 B;

由索引值index = 15 = 1111 B = (key.hash) &(table.length - 1) = (key.hash) & 1111 B,

得知,key的hash值的后4位 key.hash = 1111 B。

当数组长度扩容为32时,table.length - 1 = 32 - 1 = 31 = 0001 1111 B;

在旧表中,索引 index = 15 位置处的节点,由上述可知,其key的hash值的后四位是 1111 B。

对应到长度为32的新表中计算其索引值,index = (key.hash) &(table.length - 1)。

此时,由于key.hash高位数值的不同,最终结果存在两种可能。

1.若 key.hash = xxx0 1111 B,

则index = (key.hash) &(table.length - 1) = xxx0 1111 & 0001 1111 =0000 1111 = 15。

所以,存在原数组索引值为15桶中的元素,存到了新数组中索引值为15的桶中(索引值没变)。

2.若 key.hash = xxx1 1111 B,

则index = (key.hash) &(table.length - 1) = xxx1 1111 & 0001 1111 =0001 1111 = 31。

所以,存在原数组索引值为15桶中的元素,存到了新数组中索引值为31的桶中(索引值发生变化)。

综上所述,扩容前后的索引值存在两种对应关系。

一种是,索引值不发生变化:新索引 = 旧索引。

另一种是,索引值发生变化:新索引 = 旧索引 + 旧表长度。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/458660.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号