List、Map集合(转载:整合一些高质量文章)包括HashMap核心源码一行一行分析

本文深入探讨了Java中ArrayList的扩容机制及其与Array的区别,解释了transient关键字的作用。同时,详细阐述了HashMap的内部结构,包括成员变量如loadFactor、threshold的含义,以及put方法和resize方法的工作流程。分析了resize方法中如何优化元素的重新分配,并解释了tableSizeFor方法确保扩容后大小为2的幂的原因。此外,提到了线程安全的ArrayList实现,如Collections.synchronizedList和CopyOnWriteArrayList。最后,讨论了Java容器中ConcurrentModificationException异常的产生原因和解决策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

List集合

ArrayList 扩容机制

见链接

Java - ArrayList 中 elementData 为什么被 transient 修饰?

transient用法
原因

Array 和 ArrayList 有何区别?

  • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象
  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的
  • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有

如何实现 Array 和 List 之间的转换?
Array 转 List: Arrays. asList(array) ;
List 转 Array:List 的 toArray() 方法。

Arrays.asList()的集合方法add()和remove()报错
我们其实可以用真正的ArraysList包装一下,这样就不会报错啦

//添加数据
List<Integer> numberList = new ArrayList(Arrays.asList(1));
numberList.add(2);
//遍历
for (Integer num : numberList) {
    System.out.println(num);
}

线程安全的ArrayList

线程安全版的ArrayList的性能比较–Collections.synchronizedList与CopyOnWriteArrayList

三种线程安全的List

SynchronizedList和Vector的区别

Iterator,Iterable

见链接

RandomAccess 作用 及 binarySearch方法详解

见链接见链接

Java容器的快速报错机制 ConcurrentModificationException 异常原因和解决方法

见链接(博客里还有一些其他并发编程的知识点)

hashcode 值是如何计算的?

hashCode真的是内存地址吗
什么是hashCode 以及 hashCode()与equals()的联系
https://blog.youkuaiyun.com/imagineluopan/article/details/121852635
https://blog.youkuaiyun.com/qq_42002006/article/details/118542766

Comparable 和 Comparator的用法

见链接

TreeMap 和 TreeSet 在排序时如何比较元素? Collections 工具类中的 sort()方法如何比较元素?

  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。
  • TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。
  • Collections 工具类的 sort 方法有两种重载的形式,
    第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
    第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

Queue

SynchronousQueue实现原理

Map集合

红黑树的定义

红黑树的定义

HashMap的成员变量

HashMap的底层实现源码分析

HashMap底层实现源码分析

1. 成员变量
(1)loadFactor:装载因子。
(2)threshold:阈值。threshold的值等于table.length * loadFactor

对于this.threshold = tableSizeFor(var1) 的疑问

tableSizeFor(initialCapacity)判断指定的初始化容量是否是2的n次幂,如果不是那么会变为比指定初始化容量大的最小的2的n次幂。
但是注意,在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边界值了。有些人会觉得这里是一个bug,应该这样书写:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。
但是请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

(3)size: 存放元素的个数,注意这个不等于数组的长度

当 size > threshold 时进行扩容

2. 构造方法
public HashMap(int var1, float var2) {
    if (var1 < 0) {
        throw new IllegalArgumentException("Illegal initial capacity: " + var1);
    } else {
        if (var1 > 1073741824) {
            var1 = 1073741824;
        }

        if (var2 > 0.0F && !Float.isNaN(var2)) {
            this.loadFactor = var2;
            this.threshold = tableSizeFor(var1);
        } else {
            throw new IllegalArgumentException("Illegal load factor: " + var2);
        }
    }
}

public HashMap(int var1) {
    this(var1, 0.75F);
}

public HashMap() {
    this.loadFactor = 0.75F;
}

public HashMap(Map<? extends K, ? extends V> var1) {
    this.loadFactor = 0.75F;
    this.putMapEntries(var1, false);
}
3. 成员方法
(1)put 方法
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
    HashMap.Node[] var6;
    int var8;
    if ((var6 = this.table) == null || (var8 = var6.length) == 0) {
        var8 = (var6 = this.resize()).length;
    }

    Object var7;
    int var9;
    if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
        var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
    } else {
        Object var10;
        Object var11;
        if (((HashMap.Node)var7).hash == var1 && ((var11 = ((HashMap.Node)var7).key) == var2 || var2 != null && var2.equals(var11))) {
            var10 = var7;
        } else if (var7 instanceof HashMap.TreeNode) {
            var10 = ((HashMap.TreeNode)var7).putTreeVal(this, var6, var1, var2, var3);
        } else {
            int var12 = 0;

            while(true) {
                if ((var10 = ((HashMap.Node)var7).next) == null) {
                    ((HashMap.Node)var7).next = this.newNode(var1, var2, var3, (HashMap.Node)null);
                    if (var12 >= 7) {
                        this.treeifyBin(var6, var1);
                    }
                    break;
                }

                if (((HashMap.Node)var10).hash == var1 && ((var11 = ((HashMap.Node)var10).key) == var2 || var2 != null && var2.equals(var11))) {
                    break;
                }

                var7 = var10;
                ++var12;
            }
        }

        if (var10 != null) {
            Object var13 = ((HashMap.Node)var10).value;
            if (!var4 || var13 == null) {
                ((HashMap.Node)var10).value = var3;
            }

            this.afterNodeAccess((HashMap.Node)var10);
            return var13;
        }
    }

    ++this.modCount;
    if (++this.size > this.threshold) {
        this.resize();
    }

    this.afterNodeInsertion(var5);
    return null;
}

put 方法执行逻辑:

  • 判断table是否为null,如果是则进行初始化
  • 通过 hash^(length-1) 把元素散列到桶上
    • 如果桶上没有节点,直接插入
    • 如果桶上有节点
      • 如果链表已经是红黑树,putTreeVal()
      • 普通链表,则尾插法。插入后判断是否转红黑树
      • 如果有相同的key,替换value
  • 如果 size > threshold 则扩容
(2)resize 方法
final HashMap.Node<K, V>[] resize() {
        HashMap.Node[] var1 = this.table;
        int var2 = var1 == null ? 0 : var1.length;
        int var3 = this.threshold;
        int var5 = 0;
        int var4;
        if (var2 > 0) {
            if (var2 >= 1073741824) {
                this.threshold = 2147483647;
                return var1;
            }
            
			// 对于 已经有数据的 hashmap:put元素后 size > threshold 进行扩容
            if ((var4 = var2 << 1) < 1073741824 && var2 >= 16) {
                var5 = var3 << 1;
            }
        } else if (var3 > 0) {
        // 对于 有参构造器生成的 hashmap 进行扩容
            var4 = var3;
        } else {
        // 对于 无参构造器生成的 hashmap 进行扩容
            var4 = 16;
            var5 = 12;
        }

		// 对于 有参构造器生成的 hashmap 进行扩容
        if (var5 == 0) {
            float var6 = (float)var4 * this.loadFactor;
            var5 = var4 < 1073741824 && var6 < 1.07374182E9F ? (int)var6 : 2147483647;
        }

        this.threshold = var5;
        HashMap.Node[] var14 = (HashMap.Node[])(new HashMap.Node[var4]);
        // var14 是扩容之后的空数组
        this.table = var14;
        // 判空是因为,对于初始化的 map 集合不用执行
        if (var1 != null) {
        	// 遍历 map 集合的table数组,从下标0 到length长度
            for(int var7 = 0; var7 < var2; ++var7) {
                HashMap.Node var8;
                // var8 表示每个链表的第一个节点,只有不为null才有必要执行if
                if ((var8 = var1[var7]) != null) {
                    var1[var7] = null;
                    if (var8.next == null) {
                    	// 如果第二个节点为null(链表只有一个头节点)的执行逻辑
                    	// var8.hash & (var4 - 1) 计算元素散列的位置,即元素在新数组的下标位置
                        var14[var8.hash & var4 - 1] = var8;
                    } else if (var8 instanceof HashMap.TreeNode) {
                    	// 如果链表已经是红黑树结构了,处理逻辑如下(目前不具体分析)
                        ((HashMap.TreeNode)var8).split(this, var14, var7, var2);
                    } else {
                    	// 链表有超过1个节点的处理逻辑————变量比较多,下面我会配合图解,建议对比着看
                    	
                    	// var9 用来记录扩容后一个链表上位置不变的节点
                        HashMap.Node var9 = null;
                        HashMap.Node var10 = null;
                        // var11 用来记录扩容后一个链表上位置变化的节点
                        HashMap.Node var11 = null;
                        HashMap.Node var12 = null;

                        HashMap.Node var13;
                        //	do while 遍历整个链表
                        do {
                            var13 = var8.next;
                            // if 中的判断条件 逻辑十分巧妙,优化了重新分配元素的方法,下面有分析,可以先去看分析再来看代码。
                            if ((var8.hash & var2) == 0) {	// 说明 元素所在数组下标位置不变
                                if (var10 == null) {
                                    var9 = var8;
                                } else {
                                    var10.next = var8;
                                }

                                var10 = var8;
                            } else {	// 说明 元素所在数组下标位置要改变
                            	
                                if (var12 == null) {
                                    var11 = var8;
                                } else {
                                    var12.next = var8;
                                }

                                var12 = var8;
                            }

                            var8 = var13;
                        } while(var13 != null);

						// 元素所在数组下标位置不变。这些节点都用 var9 保存着
                        if (var10 != null) {
                            var10.next = null;
                            var14[var7] = var9;
                        }

						// 元素所在数组下标位置+原数组长度(比如从length = 4(0100) 扩容到 length = 16(0001 0000),则加上4)。这些节点都用 var11 保存着
                        if (var12 != null) {
                            var12.next = null;
                            var14[var7 + var2] = var11;
                        }
                    }
                }
            }
        }

        return var14;
    }

在这里插入图片描述

resize 方法执行逻辑:

  • 扩容 table 数组,对三种 hashmap 进行扩容
    • 已经有数据的 hashmap:put元素后 size > threshold
    • 自定义构造器生成的 hashmap
    • 无参构造器生成的 hashmap
  • 重新分配元素,对于刚初始化的hashmap不用

重新分配元素,正常的逻辑是:遍历元素,重新计算每个元素的hash值,hash&(length-1),再分配到相应的桶中。但是,源码中做出了优化。 参考文章

我们在扩充 HashMap 的时候,不需要重新计算 hash,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就可以了,是 0 的话索引没变,是 1 的话索引变成 “原位置 + 原数组长度”

体现在代码中的什么地方呢:
在resize方法中,如果你看到这样的逻辑 hashcode & length 那就是了

 if ((var8.hash & var2) == 0) { // 体现在这里
    if (var10 == null) {
         var9 = var8;
     } else {
         var10.next = var8;
     }

     var10 = var8;
 } else {
     if (var12 == null) {
         var11 = var8;
     } else {
         var12.next = var8;
     }

     var12 = var8;
 }

tableSizeFor分析:扩容后的大小一定是2的n次幂

    static final int tableSizeFor(int var0) {
        int var1 = var0 - 1;
        var1 |= var1 >>> 1;
        var1 |= var1 >>> 2;
        var1 |= var1 >>> 4;
        var1 |= var1 >>> 8;
        var1 |= var1 >>> 16;
        return var1 < 0 ? 1 : (var1 >= 1073741824 ? 1073741824 : var1 + 1);
    }

源码分析
var0 				= 0000 0011 0000 0000 0000 0000 0000 0001
var1 				= 0000 0011 0000 0000 0000 0000 0000 0000

var1 |= var1 >>> 1;
var1 >>> 1 			= 0000 0001 1000 0000 0000 0000 0000 0000
var1 |= var1 >>> 1	= 0000 0011 1000 0000 0000 0000 0000 0000

var1 |= var1 >>> 2;
var1 >>> 2 			= 0000 0000 1110 0000 0000 0000 0000 0000
var1 |= var1 >>> 2	= 0000 0011 1110 0000 0000 0000 0000 0000

var1 |= var1 >>> 4;
var1 >>> 4		 	= 0000 0000 0011 1110 0000 0000 0000 0000 
var1 |= var1 >>> 4	= 0000 0011 1111 1110 0000 0000 0000 0000

var1 |= var1 >>> 8;
var1 >>> 8 			= 0000 0000 0000 0011 1111 1110 0000 0000  
var1 |= var1 >>> 8	= 0000 0011 1111 1111 1111 1110 0000 0000

var1 |= var1 >>> 16;
var1 >>> 16			= 0000 0000 0000 0000 0000 0011 1111 1111  
var1 |= var1 >>>16	= 0000 0011 1111 1111 1111 1111 1111 1111

java7 HashMap 的死循环分析

ConcurrentHashMap 1.7 & 1.8 源码分析

为什么 Java7 采用头插法,而 Java8 却改成了尾插法?

头插法插入会改变链表的顺序,导致并发情况下可能出现环形链表的情况,而改为尾插法之后,由于新插入元素之后维持原来链表的顺序不变,不会有环形链表的情况出现,但是在并发的情况下,会出现值覆盖的情况。

java7 死循环的问题,到底是怎么产生的?

图文分析 java7 transfer方法:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍历旧表
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                //如果hashSeed变了,需要重新计算hash值
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //得到新表中的索引
                int i = indexFor(e.hash, newCapacity);
                //将新节点作为头节点添加到桶中
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
}

java7 transfer方法图片分析 有一点不严谨:

hashmap的长度应该是2的n次幂,size的长度应该满足size <= length * threshold

但是我并没有考虑这一点。

如果有其他问题,我希望大家能帮我指出来,谢谢。

在这里插入图片描述

treeMap集合(待学习)

手撕二叉树的增、删、查

手撕红黑树的增、删、查

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值