【Java集合】ArrayList源码深度分析

参考笔记:

java ArrayList源码分析(深度讲解)-CSDN博客

【源码篇】ArrayList源码解析-CSDN博客


目录

1.前言

[2. ArrayList简介](#2. ArrayList简介)

[3. ArrayList类的底层实现](#3. ArrayList类的底层实现)

[4. ArrayList的源码Debug](#4. ArrayList的源码Debug)

[4.1 使用空参构造](#4.1 使用空参构造)

(1)开始Debug

(2)初始化底层elementData数组为空数组

[(3) 对add方法中的实参进行自动装箱](#(3) 对add方法中的实参进行自动装箱)

[(4) 进入add方法](#(4) 进入add方法)

[(5)进入ensureCapcityInternal 方法](#(5)进入ensureCapcityInternal 方法)

[(6) 进入grow方法](#(6) 进入grow方法)

[(7)逐层返回,第一次扩容elementData数组完毕(0 ------> 10)](#(7)逐层返回,第一次扩容elementData数组完毕(0 ——> 10))

(8)向集合中添加第二个元素(不需要扩容)

(9)将集合中的元素添加到10个(达到第一个临界点)

(10)集合的第二次扩容开始

(11)集合的第二次扩容结束

(12)将集合中的元素添加到15个(达到第二个临界点)

(13)集合的第三次扩容开始

(14)集合的第三次扩容结束

[4.2 使用有参构造](#4.2 使用有参构造)

(1)开始Debug

(2)集合的第一次扩容(初始化)

(3)向集合中添加第一个元素

....

[5.add (E e) 方法源码](#5.add (E e) 方法源码)


1.前言

本篇博文是对单列集合 List 的实现类 ArrayList 的内容补充。之前在 List 集合的详解篇,只是拿 ArrayList 演示了 List 接口中的常用方法,并没有对它进行深究。但这正是本文要做的内容

up会利用 Debug 来一步一步地剖析 ArrayList 底层的扩容机制到底是如何实现的。重点是空参构造器构造ArrayList对象后,底层扩容机制的详细实现

注意 : ①解读源码需要扎实的基础,比较适合希望深究的同学; 不要眼高手低,看会了不代表你会了,自己能全程 Debug下来才算有收获; 本篇博文对 ArrayList 源码的解读基于主流的 JDK 8.0 的版本

2. ArrayList简介

ArrayList 类是单列集合 List 接口的一个实现类,它的本质是一个可以动态修改的数组。 ArrayList 位于 java.util.ArrayList 包下,其类的定义如下:

3. ArrayList类的底层实现

ArrayList 类在底层是由数组来实现的,ArrayList 类源码中维护了一个 Object 类型的数组 elementData,用于存储 ArrayList 集合中的元素

关于 transient 关键字,transient 本身有 "转瞬即逝、短暂的" 的意思,被 transient 关键字修饰的程序元素不可被序列化

使用空参构造来创建 ArrayList 类对象时,elementData 数组的初始容量为 0 。第一次添加元素时,将该数组容量扩容为 10 。如需再次扩容,则将 elementData 数组的当前容量扩大 1.5 倍

使用指定数组初始容量大小的带参构造来创建 ArrayList 类对象时,elementData 数组的初始容量即为**传入形参的指定容量。**如果需要扩容,则直接将该数组当前容量扩大 1.5 倍

4. ArrayList的源码Debug

4.1 使用空参构造

用以下代码为演示,来进行 Debug 操作,代码如下 :

java 复制代码
import java.util.ArrayList;
public class demo {
    public static void main(String[] args) {
        //演示 : Debug ArrayList空参构造,以及数组扩容的全流程
        //1.通过空参构造创建一个ArrayList类对象
        ArrayList arrayList = new ArrayList();
        System.out.println("使用空参构造创建的集合对象 = " + arrayList);

        //2.利用for循环向集合对象中添加10个元素(0~9)
        for (int i = 0; i < 10; i++) {
            arrayList.add(i);   //此处有自动装箱,即 int -> Integer
        }
        System.out.println("添加十个元素后,当前集合 = " + arrayList);

        //3.如果按照理论,在向集合对象中添加第11个元素时,底层的数组需要扩容 10 -> 15(1.5倍)
        arrayList.add("这是集合的第十一个元素捏.");
        arrayList.add("这是集合的第十二个元素捏.");
        arrayList.add("这是集合的第十三个元素捏.");
        arrayList.add("这是集合的第十四个元素捏.");
        arrayList.add("这是集合的第十五个元素捏.");

        //4.再次测试ArrayList类底层数组的扩容机制    (10 ---> 15 ---> 22)
        arrayList.add("这是集合的第十六个元素捏.");
        System.out.println("添加十六个元素后,当前集合 = " + arrayList);
    }
}

(1)开始Debug

首先 ,进入 Debug 界面,并在第 6 行的无参构造调用行,跳入 ArrayList 类无参构造 ,如下 GIF 图所示 :

(2)初始化底层elementData数组为空数组

跳入 ArrayList 无参构造,可以看到,它将用于存储集合元素的 elementData 数组进行了初始化。等号右边的 "DEFAULTCAPCITY_EMPTY_ELEMENTDATA" 直译过来就是 "默认容量空的elementData数组" ,可以根据 Ctrl + b/B 查看 ArrayList 中该常量的源码,如下 :

可以看到,这个所谓的 "默认容量空的elementData数组" 名副其实,确实是一个空的数组,而且与 ArrayLis t中用于存储集合元素的 elementData 数组一样都是 Object 类型

接下来,我们跳出这个无参构造,进入 for 循环,并跳入第一个元素的 add 方法

(3) 对add方法中的实参进行自动装箱

第一次跳入 add 方法,会先跳到 valueOf 方法中,对要添加的 int 类型进行装箱操作。如下图所示 :

这里不用管它,直接选择跳出,并准备第二次跳入 add 方法

第一次跳出 add 方法后,该行代码仍然标注着高亮,表示此行代码还未执行完毕。我们第二次跳入 add 方法

(4) 进入add方法

第二次跳入 add 方法,我们来到了真的"add"方法,如下 :

形参列表的 "e" 代表了当前要添加的元素,根据我们上面给出的代码,for循环要向集合对象中添加 0~9 这 10 个元素,这是 for 循环的第一次循环,要添加元素 0,因此可以看到,此时 e = 0

第一行调用的函数是 ensureCapcityInternal (minCapacity:size + 1) ,"size" 表示当前集合中的元素个数 ,此时的 size = 0 。该函数的作用是:确定是否需要对 elementData 数组进行扩容。因为我们当前正在执行添加操作嘛,所以这里传入的参数为 minCapacity: "size + 1 " 就表示要把该元素添加进去,所需的数组的最小长度

第二行就是简单的赋值操作了,把当前元素 e 添加到数组中,然后 size++

下面我们进入 ensureCapcityInternal (minCapacity:size + 1) 方法看看到底这个扩容机制到底是怎么个事

(5)进入ensureCapcityInternal 方法

进入 ensureCapcityInternal 方法,其代码如下:

可以看到,该方法中又调用了两个方法 ensureExplicitCapacity、calculateCapacity,真是跟长了包皮一样,套来套去真的烦,但是不要急躁,这里涉及到的代码都不是很难

我们先进入内层调用的函数 **calculateCapacity(elementData,minCapacity)**看到底做了啥:

可以看到第一条语句是 if(elementData == DEFAULTCAPCITY_EMPTY_ELEMENTDATA)

,判断数组是否为空数组,如果是的话就 return Math.max(DEFAULT_CAPACITY,minCapacity ),DEFAULT_CAPACITY = 10,见名知意,它表示的意思就是:默认的数组长度为 10,源码中也有注释,如下:

而 minCapicity(数组的最小容量)= 1,代表的是要把该元素添加进去,所需的数组的最小长度。所以这个 if 语句很明显的就是用于第一次扩容的代码。我们这里的返回值就是 10

回到 ensureCapcityInternal 方法,下面就是进入 ensureExplicitCapacity 方法了:

modCount 属性用于保存你修改集合的次数,用来防止有多个线程修改它,多个线程修改时会抛出异常,这里我们不用管它

下面的 if 判断语句就是利用前面得到的所需的最小数组容量 minCapacity 和当前数组的容量elementData.length 作一个比较,如果发现当前数组的容量无法满足所需的最小数组容量 minCapacity ,那就要调用 grow 方法开始真正意义上的扩容了

这里 minCapacity = 10,elementData.length = 0,所以需要调用 grow 方法完成数组扩容

(6) 进入grow方法

java 复制代码
private void grow(int minCapacity) {
    //minCapacity:10

    //记录当前数组的容量,由于此时还没有存储元素,所以为0
    int oldCapacity = elementData.length;

    /*
     核心扩容算法:原来容量的 + 原来容量的一半(即原容量的1.5倍),但第一次扩容的时候这里的newCapacity = 0
     因为第一次扩容的时候 oldCapacity = 0 嘛。但不用担心,后面紧跟着的if语句就是用来应对这种情况的
     */
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    //确保新容量newCapacity大于等于最小容量minCapacity,所以第一次扩容时 newCapacity = 10
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //下面这个if语句不用管也没事,一般都不会搞这么大容量的列表
    //判断新容量是否超过了Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //如果超过了,则将新容量赋值为Integer.MAX_VALUE
        newCapacity = hugeCapacity(minCapacity);

    //拷贝出一个新数组,新数组的容量就是前面计算的newCapacity,扩容完毕
    elementData = Arrays.copyOf(elementData, newCapacity);
}

不要因为走得太远而忘记了我们为什么出发 。我们一路追追追,只有一个目的:elementData数组目前是个空数组,要添加第一个元素 0 进入,必须先扩容,因此通过上述代码确定最终的 elmentData 数组长度为 newCapacity = 10,第一次扩容完毕

(7)逐层返回,第一次扩容elementData数组完毕(0 ------> 10)

grow 函数执行完毕,返回到 ensureExplicitCapacity 函数,如下:

可以看到, elementData 数组已经由原来的 "Object[0]@512" ---> "Object[10]@514" ,这也可以验证我们上文提到的 "当我们使用空参构造来创建 ArrayList 类对象时,elementData 数组的初始容量为 0 ,第一次添加元素时,将该数组扩容为 10 " 。这里还需要注意的是扩容之后的数组是一个新的数组,是存储在不同地址空间的,所以 @512 ---> @514

执行完 ensureExplictitCapacity 方法,接着返回到 ensureCapacityInternal 方法,如下:

执行完 ensureCapacityInternal 方法,就回到了一开始的 add 方法了,然后是执行 elementData[size++] = e ,把当前元素 e 加入到数组中,如下 GIF 图所示:

可以看到第一个元素 e = 0 成功地被添加进了 elementData 数组,size 也更新为 1,即当前 ArrayList 集合中的元素个数为 1

接着返回到测试类中,如下:

(8)向集合中添加第二个元素(不需要扩容)

第一次扩容完成后,elementData 数组的长度由 0 ---> 10 ,因此, for 循环中后续几个元素的添加都不再需要扩容。以第二个元素的添加为例,这里再过一遍,加深印象,这次主要是体验一下流程,不会像第一次一样描述得那么细致了

如下 GIF 图所示,随着循环变量i的自增,for 的第一次循环顺利结束,第二次循环开始,向集合中添加第二个元素 1 :

可以看到,还是老规矩,需要先将 int 类型的数据 1 做装箱处理

接着,再次跳入 add 方法,如下图所示 :

跟我们前面提到的一样,需要先执行 ensureCapacityInternal 函数确定数组是否需要扩容,之后再将元素 e 加入到数组中,size++

追入 ensureCapacityInternal 方法,这里我以 GIF 图展示,不然篇幅会太长:

可以看到,这里计算得到的 minCapacity = 2 ,即加入该元素所需的数组的最小长度 minCapacity = 2,而在前面我们第一次 add 时,已经将数组的长度扩容为 10 了,所以 elementData.length = 10

因此,不会进入 grow 函数,即不需要对数组进行扩容操作

执行完 ensureCapacityInternal 后,回到 add 方法,将当前元素 e = 1 加入到数组中,然后更新 size = 2,如下 GIF 所示:

(9)将集合中的元素添加到10个(达到第一个临界点)

之后的 8 次循环(向集合中添加 2~9 这 8 个数),流程均与第二次循环相同,我们直接一笔带过,如下 GIF 图所示 :

(10)集合的第二次扩容开始

因为第一次对 elementData 数组进行扩容时,默认只从 0 ---> 10 。而 for 循环结束后,我们已经向集合对象中添加了 10 个元素,即 ArrayList 底层的 elementData 数组已被装满。现在想添加第 11 个元素 ,就需要对 elementData 数组进行第二次扩容,先说结论,扩容后的数组容量扩大了 1.5 倍。即第二次扩容之后,elementData 数组为:10 --> 10 + 10 / 2 = 15

先跳入 add 方法,看看会发生什么,如下图所示 :

可以看到,由于从第 11 个元素开始,均为 String 类型,String 本身就是引用类型,因此不需要"装箱"的操作,所以直接跳入了 add 方法

跳入 ensureCapacityInternal ,判断是否需要对数组进行扩容,如下 GIF 所示:

可以看到,这里计算得到的 minCapacity = 11 ,即加入该元素所需的数组的最小长度minCapacity = 11,而当前的数组我们只在第一次 add 的时候扩容过一次,即将数组的长度扩容为10,所以此时 elementData.length = 10 ,所以需要执行 grow 函数对数组进行扩容

进入 grow 函数,对数组进行扩容:

java 复制代码
private void grow(int minCapacity) {
    //minCapacity:11

    //记录当前数组的容量,oldCapacity = 10
    int oldCapacity = elementData.length;

    /*
     核心扩容算法:原来容量的 + 原来容量的一半(即原容量的1.5倍)
     扩容之后的数组容量:newCapacity = 10 + 10/2 = 15
     */
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    //确保新容量newCapacity大于等于最小容量minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //下面这个if语句不用管也没事,一般都不会搞这么大容量的列表
    //判断新容量是否超过了Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //如果超过了,则将新容量赋值为Integer.MAX_VALUE
        newCapacity = hugeCapacity(minCapacity);

    //拷贝出一个新数组,新数组的容量就是前面计算的newCapacity = 15 ,扩容完毕
    elementData = Arrays.copyOf(elementData, newCapacity);
}

不要因为走得太远而忘记了我们为什么出发 。我们一路追追追,只有一个目的: elementData 数组当前是个容量为 10 的数组,要添加第 11 个元素字符串 e = "这是集合的第十一个元素捏" ,但是数组容量不够,所以需要先扩容。因此通过上述的 grow 函数确定扩容之后的 elmentData 数组长度为 newCapacity = 15,扩容完毕

(11)集合的第二次扩容结束

后面就是逐层返回到 add 函数了,然后将第 11 个元素 e = "这是集合的第十一个元素捏" 加入到数组中,并更新 size = 11 ,如下 GIF 图所示:

(12)将集合中的元素添加到15个(达到第二个临界点)

之后的第12到第15个元素的添加,与第 11 个元素的添加大同小异,只不过添加过程中不需要进入 grow 方法对数组进行扩容。接下来 4 个元素的添加,这里一笔带过。如下 GIF 所示:

(13)集合的第三次扩容开始

第二次扩容结束后,底层的 elementData 数组的容量由 10--->15 。而经过前面的一通操作过后,elementData 数组又满了。现在我们想向集合中添加第 16 个元素 ,就要进行集合的第三次扩容。从第二次扩容开始,之后的每次扩容在底层都与第二次扩容原理一样,并且每次都扩容到当前集合容量的 1.5 倍。即第三次扩容之后,elementData 数组为:15 --> 15 + 15 / 2 = 22

同样,直接跳入 add 方法。如下 GIF 图所示:

可以看到,第 16 个元素也为 String 类型,本身就是引用类型,因此不需要"装箱"的操作,所以直接跳入了 add 方法。size = 15 表示 ArrayList 集合中的元素个数为 15

跳入 ensureCapacityInternal ,判断是否需要对数组进行扩容:

可以看到,这里计算得到的 minCapacity = 16 ,即加入该元素所需的数组的最小长度 minCapacity = 16,而当前的数组经过前面的第二次扩容后长度为 15,所以 elementData.length = 15,所以需要执行 grow 函数对数组进行扩容

进入 grow 函数,对数组进行扩容:

java 复制代码
private void grow(int minCapacity) {
    //minCapacity:16

    //记录当前数组的容量,oldCapacity = 15
    int oldCapacity = elementData.length;

    /*
     核心扩容算法:原来容量的 + 原来容量的一半(即原容量的1.5倍)
     扩容之后的数组容量:newCapacity = 15 + 15/2 = 22
     */
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    //确保新容量newCapacity大于等于最小容量minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //下面这个if语句不用管也没事,一般都不会搞这么大容量的列表
    //判断新容量是否超过了Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //如果超过了,则将新容量赋值为Integer.MAX_VALUE
        newCapacity = hugeCapacity(minCapacity);

    //拷贝出一个新数组,新数组的容量就是前面计算的newCapacity = 22 ,扩容完毕
    elementData = Arrays.copyOf(elementData, newCapacity);
}

还是那句话,不要因为走得sssssssssssssssssss了我们为什么出发 。我们一路追追追,只有一个目的:elementData 数组当前是个容量为 15 的数组,要添加第 16 个元素字符串 e = "这是集合的第十六个元素捏" ,但是数组容量不够,所以需要先扩容。因此通过上述的 grow 函数确定扩容之后的 elmentData 数组长度为 newCapacity = 22,扩容完毕

(14)集合的第三次扩容结束

后面就是逐层返回到 add 函数了,然后将第 16 个元素 e = "这是集合的第十六个元素捏" 加入到数组中,并更新 size = 16 ,如下 GIF 图所示:

到此,无参构造的分步骤Debug演示,就到这里结束了

4.2 使用有参构造

如果用带参构造来初始化 ArrayList 对象,那么它底层的扩容机制与无参构造初始化 ArrayList 对象时的大同小异。唯一不同的一点在于,使用带参构造初始化 ArrayList 对象,底层的 elementData 数组在一开始不会置空,而是将其初始化为调用带参构造时中实参指定的长度。之后的扩容流程与空参构造初始化对象时无异。因此,这里就不会像之前空参构造时演示得那么细了

将用以下代码为演示,来进行 Debug 操作,代码如下 :

java 复制代码
import java.util.ArrayList;
public class demo {
    public static void main(String[] args) {
        //演示 : 测试通过带参构造初始化ArrayList集合时,其底层的数组扩容机制
        //1.创建ArrayList集合对象
        ArrayList arrayList = new ArrayList(4);
        System.out.println("刚创建的集合 = " + arrayList);

        //2.向集合中添加元素(先来4个)
        for (int i = 0; i < 4; i++) {
            arrayList.add(i);       //此处涉及到了自动装箱,int -> Integer
        }

        //3.再次向集合中添加元素。(扩容:4 ------> 6)
        arrayList.add("这是第五个元素捏");
        arrayList.add("这是第六个元素捏");
        System.out.println("添加六个元素后,集合 = " + arrayList);

        //4.再次向集合中添加元素。(扩容:6 ------> 9)
        arrayList.add("这是第七个元素捏");
        System.out.println("添加七个元素后,集合 = " + arrayList);
    }
}

(1)开始Debug

首先,进入 Debug 界面,并在第 6 行的有参构造调用行,跳入 ArrayList 类有参构造 ,如下GIF 所示 :

(2)集合的第一次扩容(初始化)

跳入 ArrayList 的带参构造,如下图所示 :

可以看到,elementData 数组被初始化为了长度为 4 的数组,4正是我们调用带参构造时指定的长度

ArrayList 类的该带参构造是一个 if - else if - else 的复合条件语句。因为我们指定的长度 initialCapacity = 4 > 0 ,所以它直接进入 if 控制的语句中,即将一个长度为 4 的新数组赋值给了 elementData 数组(其实就是改变了 elementData 引用的指向)。如果带参构造传入的实参为 0 ,它会将 EMPYT_ELEMENTDATA 赋值给数组,这里的 EMPYT_ELEMENTDATA 也是一个空数组,如下:

如果传入的实参 initialCapacity< 0 ,就会抛出一个异常对象

接着,跳出带参构造,回到测试类,可以看到底层的 elementData 数组被初始化为 4 的长度。如下 GIF 所示:

(3)向集合中添加第一个元素

该过程在使用无参构造时已经详细说明,所以这里我以 GIF 图的方式演示整个添加流程,对于一些重要的部分我会放慢:

以上就是添加一个元素的整个流程,和前面使用无参构造器的时候是完全一样的,对于后续的扩容机制也是完全一致的,所以这里就不再 Debug 了,不然本文的篇幅太长了

....


5.add (E e) 方法源码

这里我将 add (E e) 涉及到的所有方法集中在一起,方便查看,代码也加了详细的注释,如下:

java 复制代码
/**
 * 往ArrayList添加元素
 *
 * @param e 待添加的元素
 * @return
 */
public boolean add(E e) {
    //调用ensureCapacityInternal方法判断是否需要对数组进行扩容
    ensureCapacityInternal(size + 1);
    //扩容结束后将元素e加入到数组中,并且更新 size
    elementData[size++] = e;
    return true;
}

/**
 * 确保数组elementData容量充足
 * @param minCapacity 当前数组所需的最小容量
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

/**
 * @param minCapacity 当前数组所需的最小容量
 */
private static int calculateCapacity(int minCapacity) {
    //判断当前数组是否已被初始化
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //通过最小容量和默认容量10求出较大值 (用于第一次扩容)
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/**
 * 确保ArrayList扩容后的容量充足
 *
 * @param minCapacity 当前数组所需最小容量
 */
private void ensureExplicitCapacity(int minCapacity) {
    //集合修改次数++,该属性在扩容的过程中没用,主要是用于迭代器中,确保线程安全
    modCount++;
    //判断当前数组容量是否满足最小容量,不满足则调用grow进行进行扩容
    if (minCapacity - elementData.length > 0)
        //调用grow扩容方法
        grow(minCapacity);
}

/**
 * 扩容 elementData 数组
 *
 * @param minCapacity 当前数组所需最小容量
 */
private void grow(int minCapacity) {
    //记录数组的当前长度,此时由于木有存储元素,长度为0
    int oldCapacity = elementData.length;

    //核心扩容算法:原来容量的 + 原来容量的一半(即原容量的1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    //确保新容量newCapacity大于等于最小容量minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //下面这个if语句不用管也没事,一般都不会搞这么大容量的列表
    //判断新容量是否超过了Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //如果超过了,则将新容量赋值为Integer.MAX_VALUE
        newCapacity = hugeCapacity(minCapacity);

    //拷贝出一个新数组,新数组的容量就是前面计算的newCapacity,扩容完毕
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 获取一个超大容量
 * @param minCapacity
 * @return
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow(健壮性代码)
        throw new OutOfMemoryError();
    // 如果当前超过了 MAX_ARRAY_SIZE(Integer.MAX_VALUE-8)就返回Integer.MAX_VALUE
    // 否则返回 MAX_ARRAY_SIZE(Integer.MAX_VALUE-8)
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}
相关推荐
xiaolingting2 小时前
Java 二叉树非递归遍历核心实现
java··二叉树非递归遍历
嘵奇2 小时前
深入解析 Java 8 Function 接口:函数式编程的核心工具
java·开发语言
东方靖岚3 小时前
R语言的数据库交互
开发语言·后端·golang
一路向北North4 小时前
IDEA加载项目时依赖无法更新
java·ide·intellij-idea
小萌新上大分5 小时前
SpringCloudGateWay
java·开发语言·后端·springcloud·springgateway·cloudalibaba·gateway网关
PacosonSWJTU6 小时前
python基础-13-处理excel电子表格
开发语言·python·excel
froginwe116 小时前
Perl 条件语句
开发语言
直视太阳6 小时前
springboot+easyexcel实现下载excels模板下拉选择
java·spring boot·后端
Code成立6 小时前
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》第2章 Java内存区域与内存溢出异常
java·jvm·jvm内存模型·jvm内存区域
啥都鼓捣的小yao6 小时前
利用C++编写操作OpenCV常用操作
开发语言·c++·opencv