ArrayList详解
我们先看下Java中ArrayList的实现
据图可知:ArrayList继承自AbstractList并且实现List、RandomAccess、Cloneable、Serializable接口。但我们这里先只关注List接口。
基本概念
Java中的ArrayList是一个动态数组,它实现了List接口,提供了基于数组的数据存储方式,并允许在运行时动态地调整其大小。
基本特性
- 动态数组:ArrayList使用动态数组来存储元素,其大小可以根据元素的增加或减少而动态调整
- 随机访问:由于底层是数组,ArrayList提供了快速的随机访问能力,可以通过索引快速访问元素
- 非同步:ArrayList不是线程安全的。如果在多线程环境中使用,需要手动同步(例如,可以使用Collections.synchronizedList方法来包装ArrayList)
- 允许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实例:
- ArrayList():创建一个默认容量为10的空列表(实际上是先创建一个空列表,在第一次向列表中添加元素时,才会通过调用grow()方法来分配一个具有默认容量(通常是10)的新数组,并将旧数组中的元素复制到这个新数组中)
- ArrayList(int initialCapacity):创建一个具有指定初始容量的空列表
- 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的新的内部数组