HashMap集合核心源码分析

HashMap集合核心源码分析




  • 基础概述
  • 初始加载
  • 初次添加
  • 扩容逻辑




第一章 基础概述

第01节 理论说明

HashMap底层根据 JDK 版本的不同,分为两种情况

JDK7版本 JDK8版本
底层数据结构 数组 + 链表 数组 + 链表 + 红黑树

问题:程序代码为什么会这样设计?

这样设计主要是为了提升查询的效率。
1. 如果是 JDK7的版本,数组+链表。 当其中某个链表的结点数目非常多的情况下,查询的效率就会降低。(从头往下查找,效率低下)
2. 如果是 JDK8的版本,数组+链表+红黑树。 当链表节点的数量大于等于 8 的时候,则底层会转换成为红黑树结构,每次查询可以排除一半的数据。

说明:红黑树是平衡二叉B树。每次查询都是折半查询,一次可以排除一半的数据,大大提升查询效率。

我们这里重点分析 JDK8的底层原理,主要介绍以下几点内容

1. 底层原理简述说明
2. 初始加载(底层构造方法和成员变量说明)
3. 采用put()方法,添加第一个数据的情况
4. 底层达到扩容情况的分析




第02节 底层原理图

Hash表结构

HashMap集合核心源码分析_第1张图片






第二章 初始加载

第01节 核心成员变量

说明

HashMap集合核心源码分析_第2张图片

详解

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
     

	//IO流序列化使用的序列化ID,这里对于他,我们不作为重点关注
    @java.io.Serial
    private static final long serialVersionUID = 362498820763181265L;

	//默认的初始化容量大小,这里使用的是位移运算,左移4位相当乘以2的4次方,也就是16
	//也就是说,如果创建Hash表结构的数组的时候,初始化数组长度为16个空区域存放数据。
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

	//底层数组当中,最大的容量,也就是HashMap底层数组扩容之后,可以达到的最大容量大小
	//最大容量大小就是 (二进制的位运算)左移30位。也就是 1*2的30次方。 大约为 10亿 (但是一般很难达到)
	static final int MAXIMUM_CAPACITY = 1 << 30;
	
	
	//底层的默认加载因子,也就是达到长度的多少倍的时候,准备扩容。准备扩容的过程是受到加载因子去控制的
	//例如: 我们的初始容量是 16,默认加载因子是0.75 那么当添加数据达到 16*0.75=12 的时候,如果总节点数目超过64,就要进行扩容成为 32
	//例如: 我们扩容之容量是 32,默认加载因子是0.75 那么当添加数据达到 32*0.75=24 的时候,如果总节点数目超过64,就要进行扩容成为 64
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	
	
	//底层将链表结构转换成为红黑树结构的阈值,当添加 包含有数据的节点Node对象, 达到8的时候,准备将链表转换成为红黑树
	static final int TREEIFY_THRESHOLD = 8;
	
	//底层将红黑树结构转换成为链表结构的阈值,当移除 包含有数据的节点Node对象,低于6的时候,准备将红黑树转换成为链表
	static final int UNTREEIFY_THRESHOLD = 6;
	
	//如果HashMap集合需要进行扩容,则有两个条件。 
	//第一个是受到加载因子控制的长度达到
	//第二个是受到下面的常量 最小树容量 64控制。
	//也就是说,当加载因子的长度值达到,并且整个 Hash表结构当中,最少有64个 Node对象节点的情况下,才会进行扩容操作
	//如果没有达到 64个节点,只是加载因子控制的长度值达到,也不会进行扩容的操作,因为扩容的数组,会占据内存。
	static final int MIN_TREEIFY_CAPACITY = 64;
	
	//操作变化量,记录集合做增删操作的次数。
	//注意: 只是增删操作,才会使用到 modCount 查询和修改不会使用得到
	transient int modCount;
	
	//记录集合当中size 键值对的个数
	transient int size;
	
	//集合底层维护的数组,hash表结构当中的数组,保留的是节点 Node的数组信息
	transient Node<K,V>[] table;
	
	//扩容需要使用到的参数,该数据会以2的倍数递增。 16、32、64、128、256...
	int threshold;
	
	//记录加载因子的变量,一般情况下我们会使用默认的加载因子 0.75 但是也可以使用这个变量进行修改
	final float loadFactor;
}



第02节 核心构造方法

构造方法的代码

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
     

	//无参数构造方法,默认使用这种方式,例如: HashMap map = new HashMap<>();
	//这种情况下,底层会使用默认的加载因子赋值,默认的加载因子是 DEFAULT_LOAD_FACTOR = 0.75f
    public HashMap() {
     
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

	//带有int类型参数的构造方法,这里的int类型的参数,表示初始容量大小。会调用带有两个参数的构造方法
	//其中第一个参数 initialCapacity 直接传递给带有两参数的构造方法
	//额外的传入默认加载因子  DEFAULT_LOAD_FACTOR = 0.75f 给带有两个参数的构造方法
    public HashMap(int initialCapacity) {
     
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }


	//带有两个参数的构造方法,第一个参数是 初始化容量大小,第二个参数是加载因子
	//在构造方法当中,主要对于前面的参数进行了程序健壮性的判断,要求如下:
	//1. 初始化容量的大小,小于0的情况下,抛出非法参数异常
	//2. 初始化容量如果大于最大容量数 MAXIMUM_CAPACITY 则赋值为最大容量数目,备注:MAXIMUM_CAPACITY=10亿左右
	//也就是说 初始化容量 initialCapacity 的取值范围在 0 ~ 10亿左右的范围
	//第三个判断,表示加载因子必须是正数,而且要求必须是 float类型的数据,否则出现非法参数异常
	//最后就是给加载因子赋值,并且计算根据初始化容量,得到需要创建 数组容量大小 threshold
	//这里最为核心的代码:根据初始化容量值,得到数组的初始化大小 this.threshold = tableSizeFor(initialCapacity);
	//说明: 数组的初始化大小,必须是2的整数倍,例如:  16、32、64、128、256、512....
    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集合的情况下,将参数的map集合添加到目前创建的map集合当中。
	//例如:  HashMap one = new HashMap<>();  后面可能添加过键值对  one.put(xxx,xxx) ....
	//那么接下来,执行下面的代码  HashMap two = new HashMap<>(one); 这里将one作为参数传递
	//传递的过程当中,就是将 one 集合当中的数据,传递到 two 当中。 同时给 two指定加载因子是默认的加载因子 0.75f
	public HashMap(Map<? extends K, ? extends V> m) {
     
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    
	
	//这个方法,并不是构造方法,主要是上面的构造方法,调用此方法,完成了 one 集合数据,传递到two集合的变化
	//这个方法目前作为简单的了解即可,主要是 one 集合的数据,如何传给 two集合使用。
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
     
        int s = m.size();
        if (s > 0) {
     
            if (table == null) {
      // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
     
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }	
}

小结

1. 采用 无参数构造方法,创建对象的时候,会进行"懒加载"的操作。
	A. 懒加载:刚开始并没有创建底层的数组内容,而是只是给加载因子赋值为默认的 0.75
    B. 懒加载目的:刚开始的时候,并不会创建数组,而是在后面第一次执行添加数据的时候,才会在底层创建数组出来。如果刚开始创建对象,就创建数组,占据了内存空间
    
2. 如果采用带参数构造方法,底层会执行一个重要的方法。  
	A. 方法: this.threshold = tableSizeFor(initialCapacity);
	B. 目的: 如果我们传入的参数 capactity 不是2的次方数 163264128... 底层会帮我们修正容量的大小是 2 的次方数。
	C. 例如: 如果我们创建对象的格式是: HashMap<String,String>  one = new HashMap<>(21);  //这里的21并不是2的次方数。底层会修复为 32
	D. 例如: 如果我们创建对象的格式是: HashMap<String,String>  one = new HashMap<>(33);  //这里的21并不是2的次方数。底层会修复为 64




第03节 规范数据方法

代码

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

说明

1. 这个代码会在构造方法当中,调用得到。当我们传入的 最小容量 initialCapacity 的时候,就会传入到 cap 参数当中。
2. 由于我们编程者的传入的数据是任意的,但是底层维护的数组是要求数组的长度大小为 2的次方数 16、32、64、128、256... 这个方法就可以帮助我们修正规范数据。
3. 例如:
	当我们传递的构造方法参数如下: HashMap  one = new HashMap<>(21);  //这里的参数是10 则上面的修复方法,会将10修复为16
	当我们传递的构造方法参数如下: HashMap  one = new HashMap<>(21);  //这里的参数是21 则上面的修复方法,会将21修复为32
	当我们传递的构造方法参数如下: HashMap  one = new HashMap<>(33);  //这里的参数是33 则上面的修复方法,会将33修复为64

运算

1. 当我们传递的参数 cap = 10 的时候, 这里的 n = 9  
2. 将 n = 9 转换成为 二进制数据:   00000000 00000000 00000000 00001001
3. 下面的运算方式   n |= n >>> 1; 这里使用的是位运算。 先计算 n >>> 1 再运算  n | (前一步的结果)  最后赋值给n
4. 后面的操作依次执行....
5. 最后执行完毕之后,获取到修正之后的数据值

举例

HashMap集合核心源码分析_第3张图片




第三章 初次添加

第01节 理论说明

说明

1. 当我们执行 map.put("name","zhangsan"); 的时候,底层会调用 put() 方法。
2. 但是我们不难发现,底层的put()方法会调用一个核心方法putVal()方法,重点就是 putVal() 方法的实现

路由寻址算法

//寻址算法: 底层维护数组的长度-1 与 节点 Map添加数据的 key得到的hash值进行 按位与 & 的操作
//例如: 上面我们使用 map.put("name","zhangsan"); 那么我们假设初始容量是 16
//那么下面的计算方式就是  (16-1)& name的hash值。 当然这里的位置,可能会随着扩容的变化而改变。(目前只是初始容量为16)
(table.length - 1) & hash




第02节 核心代码

代码

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
     
	
	//当我们创建完毕HashMap集合的对象之后,会执行 map.put("name","zhangsan") 代码
	//那么底层会调用这里的 put()方法,在put()方法的里面还会调用下面的 putVal方法
    public V put(K key, V value) {
     
        return putVal(hash(key), key, value, false, true);
    }
	
	//添加方法的核心逻辑就是 putVal 方法。需要注意这个方法的返回值是 V。
	//我们知道 上面的put()方法有两种作用,如果 "name" 不存在执行添加操作, 如果 "name" 存在则执行 修改操作
	//这里传递的几个参数: 进行说明
	//1. int hash 这个主要是用于下面的寻址算法使用。 寻址算法的公式:  (目前数组长度-1)&hash 
	//2. key 表示当前添加的数据 key 这个数据会被封装成为 Node 节点的对象,保存到 tab 的 Node[] 数组当中
	//3. value 表示当前添加的数据 value 这个数据会被封装成为 Node 节点的对象,保存到 tab 的 Node[] 数组当中
	//4. onlyIfAbsent 如果是 false 则数组当中,与我们现在的key出现相同的情况下,不在添加,默认为 false
	//5. evict 当前的数据如果为 false 则底层的 table 数组处于创建模式,但是目前我们一般设置为 true 目前这个参数不做考虑
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
     
		
		//tab 表示记录一下当前的底层数组(散列表) 我们有个成员变量是 table 在这个方法当中,现在使用 tab 去记录一下
		//p   表示的当前的节点的对象
		//n   表示的是底层数组(散列表)长度大小,下面通过计算进行赋值
		//i   表示如果需要添加数据的时候,通过上面的寻址算法公式 (目前数组长度-1)&hash  计算出来的具体索引位置
        Node<K,V>[] tab; Node<K,V> p; int n, i;
		
		//如果是第一次添加的时候,数组的长度是0 还没有创建。那么则调用下面的 resize() 初始化数组长度为 16
		//这里是延迟初始化,在第一次添加数据的时候,才会去进行底层数组的初始化("懒加载" 刚开始创建对象,先不去初始化)
		//因为 hash表结构,如果刚开始创建对象就初始化,会占据大量内存空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
		
		//下面的  (n - 1) & hash 就是路由算法,计算出来 当前的元素,应该存放的索引位置 i 存放到 tab 数组当中
		//存放到数组的过程当中的时候,同时将这个对象,赋值给p
		//目前的情况是: 找到的位置刚好是所在位置 底层数组当中 为 null的情况(直接存放到数组当中)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
     
			//如果执行到 eles 当中,表示数组上面已经存在元素了,下面可能是 红黑树也可能是链表
			//如果 e 不为 null 的情况下,表示找到了一个与当前要插入的 key-value 一致的元素。
            Node<K,V> e; K k;
			
			//表示插入的数据,与我们之前添加过的数据,完全相同的情况。
			//例如: 之前的数据是 "name"="lisi" 现在我们添加的数据是 "name"="zhangsan"
			//那么后期需要进行替换的操作,也就是键相同的情况下,替换值,最终变成 "name"="zhangsan"
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
			
			//如果插入的元素,已经是 TreeNode 则表示底层是被转换成为了红黑树的结构
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
     
				//如果插入的元素,是一个链的结构,则需要进行遍历链,从头找到尾(开始索引为0,不断的获取下一个节点next)
                for (int binCount = 0; ; ++binCount) {
     
					//如果找到最后一个元素,最后一个元素的标志是 next=null的情况
					//这种情况下,就需要将对象,封装成为 Node 节点的对象,并且添加到链表的末尾位置。
                    if ((e = p.next) == null) {
     
						//创建一个新的节点,添加到当前节点(也就是最后一个节点)的末尾
						//最后一个节点的标志是 next 的值是 null
                        p.next = newNode(hash, key, value, null);
						//这里需要判断一下,是否达到了转换成为红黑树的标准,就是达到了 阈值(8-1)=7则转换红黑树
						//这里需要注意 binCount 在循环当中,是从0开始的,所以判断的条件是到7
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
							//转换成为红黑树结构
                            treeifyBin(tab, hash);
                        break;
                    }
					//如果这里条件成立,则找到了相同的key 的node元素,需要进行替换的操作
					//也就是在链里面,找到了相同的key值情况
					//也就是相当于是 之前的数据是 "name"="lisi" 现在我们添加的数据是 "name"="zhangsan"
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
			 
			//如果 e 前面的值,不为null, 需要进行替换的操作。
			//也就是相当于是 之前的数据是 "name"="lisi" 现在我们添加的数据是 "name"="zhangsan"
			//这种情况下,就是将 lisi 替换成为 zhangsan
            if (e != null) {
      // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
				//替换数据之后,返回替换的旧值
                return oldValue;
            }
        }
		
		//当hash表当中的数据进行修改的时候(增删操作的时候会做自增操作,修改和查询操作的时候,不会改变)
		//这个变量是来父类的成员变量,用于统计变化的次数。
        ++modCount;
		//随着键值对的添加,可能达到了扩容阈值的情况下,就会触发扩容之后的操作。
        if (++size > threshold)
			//进行扩容的操作
            resize();
        afterNodeInsertion(evict);
        return null;
    }
}





第四章 扩容逻辑

第01节 理论说明

说明

随着我们数据节点 Node 的添加,达到一定的条件的时候,则需要进行扩容的操作。

条件一: 加载因子达到
条件二: 总节点数目达到 默认的64

为什么需要进行扩容呢?
	在随着数据不断添加,在数组下面挂的数据会越来越多,导致最终查询的效率低下,这种情况下,就要进行扩容的操作。
	将之前的数组,扩容成为新的数组,翻倍处理。例如:之前的长度是 16 则扩容之后变成了 32
	扩容的过程当中,就会转移一部分的数据到 新的容量当中。
	这种做法,就是浪费一部分的内存空间,达到节省查询的时间的效果。

效果图

HashMap集合核心源码分析_第4张图片





第02节 核心代码

代码实现

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
     
	
    final Node<K,V>[] resize() {
     
		
		//oldTab 表示记录扩容之前的 hash表
		//oldCap 表示记录之前的容量大小,例如:之前的容量大小是 16
		//oldThr 表示记录扩容之前的阈值,例如:之前的阈值是 16*0.75=12
		//newCap 表示进行扩容操作之后,需要达到的新大小,例如:扩容后,我们需要达到 32
		//newThr 表示进行扩容操作之后,需要达到的新阈值,例如:扩容后,我们的新阈值是 32*0.75=24
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
		
		//这里表示的是HashMap当中 hash表,已经初始化了。例如:多次执行put方法之后的效果
        if (oldCap > 0) {
     
			//如果扩容已经达到了近 10亿的大小,则设置扩容的数组,为Integer类型最大值 21亿
            if (oldCap >= MAXIMUM_CAPACITY) {
     
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
			
			//如果没有达到 10亿的大小,则翻一倍(左移一位) newCap 在oldCap的基础上左移一位,数据翻倍
			//并且扩容前的大小必须大等于默认情况下的大小 16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
				//新的阈值也翻一倍,也就是之前的阈值是 16*0.75=12 现在的阈值变成了 32*0.75=24
				//我们发现新的阈值,也是在旧的阈值基础上翻一倍 12变成了24  数据是进行左移操作
                newThr = oldThr << 1; // double threshold
        }
		
		//oldThr>0 这种情况下主要是在创建HashMap集合对象的过程当中,没有使用空参数构造
		//而是采用其他构造方法创建的对象,则满足 else if(oldThr>0)的情况。 
		//这里的 newCap 就是 2的次方数, 也就是 16、32、64、128....
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
		//这里就是采用无参数构造方法创建对象的时候,还没有执行 put()方法的情况下
		//给底层维护的数组,添加默认的数据(初始化容量是16,初始化阈值是16*0.75=12)
        else {
                    // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
		
		//只要 newThr 没有赋值的情况下,都是0,这种情况下,需要进行计算。
		//也就是我们如果创建对象的情况下,使用的是 HashMap map = new HashMap<>(3);
        if (newThr == 0) {
     
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
        }
		
		//将计算出来的newThr 赋值给扩容阈值,下次需要按照新的扩容阈值进行扩容的操作。
        threshold = newThr;
		
		//===================================================================
		
		//创建出一个更长更大的数组出来,按照新的 newCap 创建。这里的newCap必须是 2的次方数 16、32、64...
        @SuppressWarnings({
     "rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
		
		//将新创建出来的数组, 记录一份地址给 table, 这里的 table 也就是 HashMap集合成员变量,Node数组
        table = newTab;
		
		//oldTab表示记录扩容之前的 hash表,这个表当中不为null,则表示存在数据。
        if (oldTab != null) {
     
			//如果存在数据,循环遍历 Hash表的数组。从数组的0号索引位置开始遍历,一直到之前哈希表数组容量的末尾
            for (int j = 0; j < oldCap; ++j) {
     
				//定义节点对象,去接收 数组的元素,这里节点,可能是具体元素,可能是链表头,可能是红黑树根节点
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
     
					//设置值为空值的情况下,方便于GC回收
                    oldTab[j] = null;
					
					//情况一:数组位置,没有任何数据的情况,直接计算出来位置,根据路由寻址算法,得到索引进行存放
                    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
						//情况三:数组位置,如果节点位置上是 链表结构
						//使用高位链 和 低位链进行处理。
						//这里使用的是寻址算法 (数组长度-1)&hash值,分别得到低位0 和 高位1
						//低位链,存放的下标位置 与当前的数组下标位置一致
                        Node<K,V> loHead = null, loTail = null;
						//高位链,存放的下标位置 当前的下标位置+扩容之前的长度值。
                        Node<K,V> hiHead = null, hiTail = null;
						//例如: 原始长度为16的数组,在10号索引位置,扩容后,新容量是32。
						//我们进行寻址算法,得到结果有 0和1的区分。     
						// 01110&11111=01110 ---> 最前面计算的结果是 0 如果是0则存放低位链,还是在10号位置
						// 11110&11111=11110 ---> 最前面计算的结果是 1 如果是1则存放高位链,存放到10+16=26号位置
						
                        Node<K,V> next;
                        do {
     
                            next = e.next;
							//计算的结果是0,则存放低位链
                            if ((e.hash & oldCap) == 0) {
     
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
							//计算的结果不是0,则存放高位链
                            else {
     
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
						
						//当上面的循环走完了,低位链走到最后的一个元素,如果不为null,则需要调整空值
						//表示下面不再连其他的元素了,也就是低位链的最后一个元素。
                        if (loTail != null) {
     
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
						
						//当上面的循环走完了,高位链走到最后的一个元素,如果不为null,则需要调整空值
						//表示下面不再连其他的元素了,也就是高位链的最后一个元素。
                        if (hiTail != null) {
     
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
}






你可能感兴趣的:(JavaSE,java,数据结构)