【数据结构】ArrayList详解

ArrayList详解

我们先看下Java中ArrayList的实现

据图可知:ArrayList继承自AbstractList并且实现List、RandomAccess、Cloneable、Serializable接口。但我们这里先只关注List接口。

基本概念

Java中的ArrayList是一个动态数组,它实现了List接口,提供了基于数组的数据存储方式,并允许在运行时动态地调整其大小。

基本特性

  1. 动态数组:ArrayList使用动态数组来存储元素,其大小可以根据元素的增加或减少而动态调整
  2. 随机访问:由于底层是数组,ArrayList提供了快速的随机访问能力,可以通过索引快速访问元素
  3. 非同步:ArrayList不是线程安全的。如果在多线程环境中使用,需要手动同步(例如,可以使用Collections.synchronizedList方法来包装ArrayList)
  4. 允许null元素:ArrayList允许在列表中存储null值

构造方法

在学习构造方法之前,我们先看下ArrayList类中的一些变量

transient Object[] elementData;

这是ArrayList内部的数组

private static final int DEFAULT_CAPACITY = 10;

注意:这是ArrayList的默认初始容量,但是需要注意,这个值并不直接用于构造一个具有10个元素的数组;它只是在需要扩容时作为参考

private static final Object[] EMPTY_ELEMENTDATA = {};

这是一个空数组,用于表示一个空的ArrayList(注意,这不是我们下面说的DEFAULTCAPACITY_EMPTY_ELEMENTDATA)

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这是一个特殊的空数组,与EMPTY_ELEMENTDATA类似,但它是专门用于默认的构造方法的,表示ArrayList尚未添加任何元素,并且此时它的容量是未定义的。这样做的目的是减少内存占用,因为初始时不需要分配一个完整的10个元素的数组

当我们第一次向这个ArrayList添加元素时,ArrayList会通过调用grow()方法(或类似的内部方法)来分配一个具有默认容量(通常是10)的新数组,并将旧数组中的元素复制到这个新数组中。这个过程是自动的,我们不需要手动干预

因此,虽然ArrayList()构造方法创建了一个看似空的列表(实际上是一个空数组),但ArrayList的内部逻辑确保了当您开始添加元素时,它会根据需要自动扩容到适当的大小(通常是10)

ArrayList提供了几个构造方法,用于创建不同类的ArrayList实例:

  1. ArrayList():创建一个默认容量为10的空列表(实际上是先创建一个空列表,在第一次向列表中添加元素时,才会通过调用grow()方法来分配一个具有默认容量(通常是10)的新数组,并将旧数组中的元素复制到这个新数组中)
  2. ArrayList(int initialCapacity):创建一个具有指定初始容量的空列表
  3. ArrayList(Collection<? extends E> c):创建一个包含指定列表集合的元素的列表,这些元素按照集合的迭代器返回的顺序排列

代码示例:

1.ArrayList()

public static void main(String[] args) {
        //创建一个空的ArrayList
        List<Integer> list=new ArrayList<>();
        //添加元素到ArrayList中
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list);//[1, 2, 3]
    }

2.ArrayList(int initialCapacity)

public static void main(String[] args) {
        //创建一个带有初始容量(这里为20)的ArrayList
        List<Integer> list=new ArrayList<>(20);
        for(int i=1;i<=10;i++){
            list.add(i);
        }
        System.out.println(list);//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }

3.ArrayList(Collection<? extends E> c)

public static void main(String[] args) {
        //创建一个包含初始元素的集合
        List<Integer> list1=new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);
        System.out.println(list1);//[1, 2, 3]
        //使用原始集合创建一个新的ArrayList
        List<Integer> list2=new ArrayList<>(list1);
        System.out.println(list2);//[1, 2, 3]
    }

主要方法

ArrayList继承了List接口的大多数方法,并提供了一些特有的实现。

我们先看一下有哪些常用方法:

1.boolean add(E e):尾插e

2.void add(int index,E element):将e插入到下标为index的位置

3.int size():返回列表中的元素个数

4.boolean isEmpty():判空

5.boolean contains(Object o):如果列表包含指定的元素,则返回true

6.E get(int index):返回下标为index处的元素

7.E set(int index,E element):将下标为index处的元素设置为element

8.boolean remove(Object o):移除列表中第一个为o的元素

9.E remove(int index):删除下标为index处的元素

10.int indexOf(Object o):返回 第一个o所在下标

11.int lastIndexOf(Object o):返回 最后一个o所在下标

12.List<E> subList(int fromIndex,int toIndex):返回一个区间为[fromIndex,toIndex)的视图

13.Object[] toArray():将列表中的所有元素转换为一个Object类型的数组

14.<T> T[] toArray(T[] a]:将列表中的所有元素转换为一个指定类型的数组

15.Iterator<E> iterator():返回按适当顺序在列表的元素上进行迭代的迭代器

1.boolean add(E e):在列表的末尾添加指定的元素

public static void main(String[] args) {
        //创建一个空的ArrayList
        List<Integer> list=new ArrayList<>();
        //添加元素到ArrayList中
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list);//[1,2,3]
    }

2.void add(int index,E element):在列表的指定位置插入指定的元素

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(1,100);//[1, 100, 2, 3]
        System.out.println(list);
    }

3.int size():返回列表中的元素个数

public static void main(String[] args) {
        //创建一个空的ArrayList
        List<Integer> list=new ArrayList<>();
        System.out.println(list.size());//0
        //添加元素到ArrayList中
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list.size());//3
    }

4.boolean isEmpty():如果列表为空,则返回true

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        System.out.println(list.isEmpty());//true
        list.add(1);
        System.out.println(list.isEmpty());//false
    }

5.boolean contains(Object o):如果列表包含指定的元素,则返回true

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        System.out.println(list.contains(0));//false
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list.contains(1));//true
    }

6.E get(int index):返回列表中指定位置的元素

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(list.get(1));//2
    }

7.E set(int index,E element):用指定的元素替换列表中指定位置的元素

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

8.boolean remove(Object o):从列表中移除指定元素的第一个匹配项(如果存在)

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.remove((Integer)3);//如果不使用强制转换,会将3看作下标 这里会越界报错
        System.out.println(list);//[1, 2]
    }

9.E remove(int index):移除列表中指定位置的元素

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.remove(3);//由于我们List列表中的元素都是Integer类型 这里的3会被看作下标(越界)
        System.out.println(list);
    }
public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.remove(2);
        System.out.println(list);//[1, 2]
    }

10.int indexOf(Object o):返回指定元素在列表中首次出现的索引,如果列表不包含该元素,则返回-1

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(3);
        System.out.println(list.indexOf(4));//-1
        System.out.println(list.indexOf(2));//1
    }

11.int lastIndexOf(Object o):返回指定元素在列表中最后一次出现的索引,如果列表不包含该元素,则返回-1

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

12.List<E> subList(int fromIndex,int toIndex):返回列表中指定的[fromIndex,toIndex)区间的部分视图

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
//注意:subList方法是左闭右开的
        System.out.println(list.subList(0,5));//[1, 2, 3, 4, 5]
    }

13.Object[] toArray():将列表中的所有元素转换为一个Object类型的数组。由于所有类都是Object的子类,因此这个方法可以适用于任何类型的列表

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Object[] arr=list.toArray();
        System.out.println(Arrays.toString(arr));//[1, 2, 3]
    }

14.<T> T[] toArray(T[] a]:将列表中的所有元素转换为一个指定类型的数组。如果指定的数组足够大以容纳列表中的所有元素,那么列表中的元素将被复制到该数组中,并且该数组将被返回。否则,将根据需要分配一个新的数组,其运行时类型与指定数组的运行时类型相同,并且该数组将被返回

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        //创建一个等于 列表大小 的数组
        Integer[] array1=new Integer[list.size()];//指定的数组
        Integer[] str1=list.toArray(array1);
        System.out.println(Arrays.toString(str1));//1 2 3

        //创建一个大于 列表大小 的数组
        Integer[] array2=new Integer[list.size()+1];
        Integer[] str2=list.toArray(array2);
        System.out.println(Arrays.toString(str2));//1 2 3 null

        //创建一个小于 列表大小 的数组
        Integer[] array3=new Integer[list.size()-1];
        Integer[] str3=list.toArray(array3);
        System.out.println(Arrays.toString(str3));//1 2 3
    }

15.Iterator<E> iterator():返回按适当顺序在列表的元素上进行迭代的迭代器

public static void main(String[] args) {
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Iterator<Integer> iterator=list.iterator();
        while(iterator.hasNext()){
            System.out.print(iterator.next()+" ");//1 2 3 
        }
    }

我们来看一个与List接口中不同的方法

·public void trimToSize():将ArrayList实例的容量调整为当前列表的”大小”(即元素的实际数量)。这意味着如果ArrayList当前存储的元素少于其内部数组的容量,那么trimToSize方法会创建一个新的数组,其大小正好等于列表中元素的数量,并将现有元素复制到这个新数组中

我们现在看一下该方法具体实现:

1.modCount++ :这是用来记录ArrayList结构上的修改次数。每当ArrayList发生结构性改变(如添加或删除元素)时,modCount都会增加。这个机制主要用于快速失败(fail-fast)行为,在迭代过程中检测并发修改。然而,在trimToSize方法中,尽管内部数组(elementData)可能会改变,但列表的元素数量和内容并没有改变,所以这里的modCount增加主要是为了保持一致性,尽管它在这个特定方法中的直接影响可能不大

2.if(size<elementData.length) :这个条件检查当前列表的大小size 是否小于内部数组 (elementData)的容量(elementData.length)。如果条件为真,说明内部数组有额外的空间没有被使用,因此可以进行缩减。

3.elementData=(size==0)?EMPTY_ELEMENTDATA:Arrays.copyOf(elementData,size)

·如果列表为空(size==0),则将elementData设置为EMPTY_ELEMENTDATA。EMPTY_ELEMENTDATA是一个空数组,用于表示空列表。这是为了节省内存,避免在空列表时仍然持有一个不必要的大数组

·如果列表不为空,则使用Arrays.copyOf(elementData,size)创建一个新的数组,其大小正好等于列表的大小(size),并将现有元素复制到这个新数组中。这样,内部数组的大小就被调整到了与列表的实际大小相匹配

trimToSize()方法可以在以下情况下使用:

·当我们确定ArrayList将不再增长,并且希望减少内存占用的时候

·在某些特定情况下,当我们希望避免ArrayList的自动扩容机制带来的性能开销时(尽管这种情况相对较少)

注意:频繁地调用trimToSize方法可能会降低性能,因为每次调用都会创建一个新的数组并复制现有元素。因此,通常建议只在确实需要时才调用此方法

扩容机制

扩容触发条件

·添加元素时容量不足:当向ArrayList中添加元素,而其当前容量不足以容纳新元素时,ArrayList会自动进行扩容操作

·显式调用ensureCapacity方法:可以通过调用ArrayList的ensureCapacity方法来显式地增加其容量。如果指定地容量大于当前容量,ArrayList会进行扩容以满足新的容量需求。我们这里

扩容策略

在大多数情况下,ArrayList的扩容策略是将当前容量增加到原来的1.5倍(即新容量=旧容量+旧/2)。这种策略可以在一定程度上减少扩容操作的频率,从而提高性能。但请注意,具体的扩容策略可能因Java版本和具体实现而有所不同,一会我们会基于2021.1版本的扩容策略进行讲解。在某些情况下(例如,当原数组长度小于某个阈值时),新数组的容量可能是原数组长度的两倍或其他倍数

代码实现:

我们先解释一下上图代码:

例如:当我们调用add方法添加元素时,可能需要使用grow方法进行扩容

1.int minCapacity:这是ArrayList在扩容后至少应该能够容纳的元素数量

2.条件判断: if(oldCapacity>0||elementData!= DEFAULTCAPACITY_EMPTY_ELEMENTDATA)

·这个条件判断用于区分ArrayList是已经被初始化过(即已经添加过元素,或者通过非默认构造方法创建) 还是刚刚被创建且还没有添加过元素。如果ArrayList已经被初始化,则进入if语句代码块

· DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个特殊的空数组,用于在ArrayList刚刚创建且没有指定初始容量时作为elementData的初始值。这样做是为了节省空间,因为在刚创建ArrayList对象时还不需要一个真正大小的数组

3.扩容逻辑

如果ArrayList已经被初始化过(即满足条件判断中的任一条件),则执行以下步骤:

  ·计算新的容量newCapacity。这是通过调用ArraysSupport.newLength方法完成的,该方法接收当前容量oldCapacity、所需增长量minCapacity-oldCapacity以及一个“首选增长量”(这里是当前容量的一半,即oldCapacity>>1)。ArraysSupport.newLenght方法基于这些参数计算出一个新的容量值,该值通常会比所需的最小容量大,以流出一些空间供未来增长使用。比如:当我们使用add方法添加元素时,此时列表中的元素个数(size)刚好超过内部数组长度(elementData.length),此时的minCapacity(size+1)-oldCapacity(elementData.length)就等于1。所以新的数组大小就为1.5倍的原数组(oldCapacity)大小

  ·使用Arrays.copyOf方法创建一个新的数组,其容量为newCapacity,并将旧数组elementData的内容复制到新数组中

  ·将elementData引用更新为新创建的数组

如果ArrayList还没有被初始化过(即不满足条件判断中的任一条件),则直接创建一个新的数组,其容量为Math.max(DEFAULT_CAPACITY, minCapacity)。这里的DEFAULT_CAPACITY是ArrayList的默认初始容量(10)。这样做是为了确保新数组至少能够容纳所需的最小容量,同时也不会小于默认的初始容量。

在默认构造方法使用后,elementData便被设置为空(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)。在后续添加元素时,才会执行此处的代码,将elementData的容量设置为初始容量(10)

4.返回值

·方法返回更新后的elementData数组

总结:grow方法负责在ArrayList需要更多空间时扩容其内部数组。它根据当前容量和所需的最小容量来计算新的容量,并创建一个新的数组来存储元素。这个新数组随后成为ArrayList的新的内部数组

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值