ArrayList详解及扩容源码分析

简介: ArrayList详解及扩容源码分析

前言


在集合框架中, ArrayList 是一个普通的类,实现了 List 接口,具体框架图如下:

1667913744781.jpg

1. ArrayList 实现了 RandomAccess 接口,表明 ArrayList 支持随机访问

2. ArrayList 实现了 Cloneable 接口,表明 ArrayList 是可以 clone 的

3. ArrayList 实现了 Serializable 接口,表明 ArrayList 是支持序列化的

4. 和 Vector 不同, ArrayList 不是线程安全的,在单线程下可以使用,在多线程中可以选择 Vector 或者CopyOnWriteArrayList

5. ArrayList 底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表


一、ArrayList的三个构造方法解析

方法 解释

ArrayList()

无参构造

ArrayList (Collection<? extends E> c)

利用其他 Collection 构建 ArrayList

ArrayList (int initialCapacity)

指定顺序表初始容量


public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
        ArrayList<Integer> arrayList2 = new ArrayList<>(12);
        arrayList1.add(1);
        ArrayList<Integer> arrayList3 = new ArrayList<>(arrayList1);
        System.out.println(arrayList3);
    }

注意:使用ArrayList(Collection<? extends E> c)这个构造方法时候,因为这里是通配符的上界,所以注意传入的类型必须是E或者E的子类。


二、ArrayList是如何扩容的?(源码分析)


首先看不带参数的构造方法

1667913821232.jpg

1667913833669.jpg

DEFAULT_CAPACITY = 10 默认容量为10;transient Object[] elementData,类似于顺序表背后的数组;private int size,size为有效元素的个数,此时为0,说明DEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度为0,这个构造方法没有分配数组内存。


那么现在就有一个问题,DEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度为0 与 默认容量为10为什么是矛盾的呢?此时没有大小,那第一次add添加数据的时候是怎么放进去的呢?


接下来,我们继续点击进入add方法里面:

1667913872072.jpg


说明存储元素的时候,默认放到最后一个元素的后边的。但是这里用到了一个ensureCapacityInternal(size + 1)方法,我们在点击进入,

1667913882921.jpg

此时size为0,当size+1传进入的时候,这个时候, minCapacity为1;那么这里又有两个方法,这个calculateCapacity(elementData, minCapacity)方法的返回值  要作为 ensureExplicitCapacity()这个方法的参数。


那么我们再点击进入calculateCapacity(elementData, minCapacity)方法,这里顾名思义就是计算容量,此时minCapacity为1,因为此时调用的是没有带参数的构造方法,那么此时 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA。


1667913896984.jpg


此时 If 语句满足,那么就会求最大值,此时  minCapacity为1,DEFAULT_CAPACITY = 10,所以第一次调用add的时候,这个时候就会返回10。那么这个时候,返回值10就会作为ensureExplicitCapacity()的参数,那么我们再点击进入这个方法内部:


1667913908660.jpg


此时 minCapacity为10,此时 elementData这个数组的长度为0,那么10-0 > 0,那么就会调用grow这个函数,把10传过去。我们再点击grow进入这个函数内部:


1667913920365.jpg


此时oldCapacity、newCapacity通过计算出来都是0,因为0 - 10 < 0的,那么此时newCapacity = minCapacity = 10。接下来再看下面一个if语句:


1667913930801.jpg


此时MAX_ARRAY_SIZE为 int 的最大值减8。这个时候这个if语句进不来,继续往下执行,那么此时这个数组才真正的分配内存。

elementData = Arrays.copyOf(elementData, newCapacity);
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

相当于 ensureCapacityInternal(size + 1)这个方法走完,数组的容量才是10,然后才能把元素放进去。所以可以总结,前提是调用不带参数的构造方法,第一次add的时候,默认容量才是10。


那么它是如何扩容的呢?比如在放第11个元素的时候?


接下来,我们还是继续看grow这个函数。

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //按照1.5倍方式扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果用户需要扩容大小 超过 原空间1.5倍,按照用户所需大小扩容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果需要扩容大小超过MAX_ARRAY_SIZE,重新计算容量大小 
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

int newCapacity = oldCapacity + (oldCapacity >> 1);其实这一句代码就是在进行扩容,而且是1.5倍的扩容。


其实带参数的构造方法,初始值给的多少,那么初始容量就是多少:

1667913960791.jpg

ArrayList(Collection<? extends E> c)这种构造方法,其实是进行的数组拷贝:


1667913970183.jpg

小结:


1. 检测是否真正需要扩容,如果是调用grow准备扩容

2. 预估需要库容的大小 :初步预估按照1.5倍大小扩容 ;如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容 ;真正扩容之前检测是否能扩容成功,防止太大导致扩容失败。


三、ArrayList的常用方法


方法 解释

boolean add(E e)

尾插e

void add (int index, E element)

 e 插入到 index 位置

boolean addAll (Collection<? extends E> c)

尾插 c 中的元素

E remove (int index)

删除 index 位置元素

boolean remove (Object o)

删除遇到的第一个 o

E get (int index)

获取下标 index 位置元素

E set (int index, E element)

将下标 index 位置元素设置为 element

void clear ()

清空

boolean contains (Object o)

判断 o 是否在线性表中

int indexOf (Object o)

返回第一个 o 所在下标

int lastIndexOf (Object o)

返回最后一个 o 的下标

List<E> subList (int fromIndex, int toIndex)

截取部分 list


这里需要注意List<E> subList(int fromIndex, int toIndex)这个方法:

public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
        arrayList1.add(1);
        arrayList1.add(2);
        arrayList1.add(3);
        arrayList1.add(4);
        arrayList1.add(5);
        List<Integer> list = arrayList1.subList(1,3);
        System.out.println(list);//2,3
        list.set(0,99);
        System.out.println(arrayList1);// 1,99,3,4,5
        System.out.println(list);//99,3
}

这个会发现,如果修改list里面的数据,那么arraylist里面的数据也会被修改。来看一张内存图就会明白怎么回事。

1667914141427.jpg


四、ArrayList的三种遍历


ArrayList 可以使用三方方式遍历:for循环+下标、foreach、使用迭代器


public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
        arrayList1.add(1);
        arrayList1.add(2);
        arrayList1.add(3);
        arrayList1.add(4);
        arrayList1.add(5);
        int size = arrayList1.size();
        for (int i = 0; i < size; i++) {
            System.out.print(arrayList1.get(i)+" ");
        }
        System.out.println();
        for (int x : arrayList1) {
            System.out.print(x+" ");
        }
        System.out.println();
        Iterator<Integer> it =  arrayList1.iterator();
        while (it.hasNext()) {
            System.out.print(it.next()+" ");
        }
}


五、ArrayList的缺陷


ArrayList底层是使用数组来存储元素的, 由于其底层是一段连续空间,当 在 ArrayList 任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后 搬移,时间复杂度为 O(n) ,效率比较低,因此 ArrayList 不适合做任意位置插入和删除比较多的场景。而且,在扩容之后,可能会带来空间的浪费。 ArrayList适合在给定了下标位置的情况下进行查找元素,此时时间复杂度可以达到O(1)。 因此: java 集合中又引入了 LinkedList ,即链表结构。


相关文章
|
7月前
|
Java
ArrayList扩容机制的相关面试题
ArrayList扩容机制的相关面试题
43 1
|
3月前
|
Java 大数据
ArrayList扩容机制
ArrayList扩容机制
|
6月前
|
存储 Java 索引
ArrayList 的扩容机制
ArrayList 的扩容机制
|
6月前
ArrayList扩容机制~
ArrayList扩容机制~
|
8月前
|
存储 Java 索引
ArrayList源码分析
ArrayList源码分析
35 1
|
8月前
|
Java 大数据
|
10月前
ArrayList初始化及扩容机制(源码分析)
ArrayList初始化及扩容机制(源码分析)
50 0
|
10月前
ArrayList与LinkedList移除指定元素对比(源码分析)
ArrayList与LinkedList移除指定元素对比(源码分析)
32 0
|
10月前
|
Java
java源码分析:ArrayList的扩容机制
ArrayList是java中最常用的集合之一,底层的数据结构是数组,在学习集合时,阅读源码是必不可少的环节之一,阅读源码可以有效的帮助我们深入了解其工作原理,下面根据源码详细的介绍下扩容机制,环境为jdk1.8。
94 0