ArrayList详解及扩容源码分析

系列文章目录

线性表之动态数组(ArrayList)的自实现_crazy_xieyi的博客-CSDN博客


文章目录

  • 前言
  • 一、ArrayList的三个构造方法解析
  • 二、ArrayList是如何扩容的?(源码分析)
  • 三、ArrayList的常用方法
  • 四、ArrayList的三种遍历
  • 五、ArrayList的缺陷

前言

在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下:
eec8d1d61fdc47a5a0fe3d7b162aa8ba.png
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是如何扩容的?(源码分析)

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

67640c91c1444a36856b8cd8ec831e39.png

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

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

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

940de45824244ccfb3a1c4746caea128.png

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

429bf406436e47079e682822d6a06420.png

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

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

7e18fd6ccf074c8cb9c77f8df16f3784.png

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

2f6ef0e8e7b7423e8489a0fe8b80ddda.png

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

213403d431564ee1aa280e4c85904b32.png

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

f65b5f8952834cdf94f4e7b94ed7ca48.png

此时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倍的扩容。 

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

fdf9cd17f1aa4e7d81f360aa5786da21.png

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

0176a7e1d0ad42a09edd2f82e7f667a1.png

 小结:

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里面的数据也会被修改。来看一张内存图就会明白怎么回事。

 2ba364afaa4c4abda3a4a5035876dfd5.png

 

四、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,即链表结构。

 

 

评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

crazy_xieyi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值