1、arraylist 概述
1.1、arraylist 介绍
arraylist
即动态数组,是最常用的 list 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。用 msdn(microsoft developer network,微软开发者网络)中的说法,就是 array 的复杂版本。
arraylist
底层使用数组实现,由于数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。所以当从 arraylist
的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
arraylist
的每个实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是大于等于列表的大小。随着向arraylist
中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造arraylist
时指定其容量。此外,arraylist
在被添加大量元素前,应用程序也可以使用 ensurecapacity()
操作来指定arraylist
实例的容量,这可以减少递增式再分配的数量。
arraylist
是非线程安全的,只能在单线程环境下使用,多线程环境下可以考虑用 collections.synchronizedlist(list l)
函数返回一个线程安全的 arraylist
类,也可以使用 java.util.concurrent
并发包下的 copyonwritearraylist
类。
1.2、arraylist 特点
arraylist
是一个可变大小的数组,可以动态地调整大小。arraylist
有以下特点:
- 动态数组:
arraylist
底层是一个动态数组,能够自动调整其大小。当添加新元素时,如果内部数组空间不足,arraylist
会创建一个更大的数组并将旧数组中的元素复制到新数组中; - 有序:
arraylist
保持元素的插入顺序,即按照元素添加到集合中的顺序来存储它们; - 快速随机访问:由于底层是数组,可以通过索引快速访问元素,时间复杂度为
o(1)
; - 可变大小:
arraylist
的大小是动态调整的,用户不需要指定其初始大小,但可以通过构造函数指定初始容量; - 不保证线程安全:
arraylist
不是线程安全的。如果多个线程同时访问一个arraylist
实例,并且至少一个线程修改了列表的结构,那么必须在外部实现同步; - 允许
null
元素:arraylist
允许存储null
元素; - 频繁的增删操作效率较低:由于插入或删除元素时需要移动数组中的其他元素,频繁的插入和删除操作会导致性能下降,特别是在列表的中间位置进行操作时,时间复杂度为
o(n)
。
1.3、arraylist 使用用例
arraylist
是 java 中广泛使用的集合类,适用于各种场景。以下是常见的使用用例和代码示例,创建一个 arraylist
,并进行基本的增删查操作::
import java.util.arraylist;
public class arraylistbasicoperations {
public static void main(string[] args) {
// 创建一个 arraylist
arraylist<string> fruits = new arraylist<>();
// 添加元素
fruits.add("apple");
fruits.add("banana");
fruits.add("cherry");
// 获取元素
string fruit = fruits.get(1); // "banana"
system.out.println("the second fruit is: " + fruit);
// 删除元素
fruits.remove("apple");
// 遍历列表
for (string item : fruits) {
system.out.println(item);
}
// 检查列表大小
int size = fruits.size();
system.out.println("list size: " + size);
}
}
1.4、arraylist 实现原理
arraylist
是 java 中基于动态数组实现的集合类,其实现原理主要包括以下几个方面:数据结构、扩容机制、添加和删除元素的过程,以及一些常见操作的时间复杂度。
- 数据结构:
arraylist
底层使用一个数组来存储元素。它定义了一个初始容量,当元素数量超过当前容量时,会自动扩容; - 扩容机制:当向
arraylist
中添加元素且当前数组已满时,会触发扩容机制。扩容时,新的数组容量通常是旧容量的 1.5 倍; - 添加元素:添加元素有两种方式:在末尾添加和在指定位置添加。前者是最常见的操作,时间复杂度为
o(1)
;后者需要移动指定位置之后的元素,时间复杂度为o(n)
; - 删除元素:删除元素同样有两种方式:删除指定位置的元素和删除指定元素的第一次出现。删除指定位置的元素需要移动后面的元素,时间复杂度为
o(n)
;删除指定元素的第一次出现则需要先查找,时间复杂度为o(n)
。
1.5、arraylist 优缺点及适用场景
优点:
- 动态调整大小:
arraylist
可以根据需要动态调整其大小,用户不需要手动管理数组的大小; - 快速随机访问:由于底层是数组,
arraylist
支持快速的随机访问,时间复杂度为 o(1); - 有序:
arraylist
保持元素的插入顺序,这使得它非常适合需要保持顺序的场景; - 实现简单:
arraylist
的实现相对简单,提供了丰富的 api,使用起来非常方便; - 允许重复和 null 元素:
arraylist
允许存储重复元素和 null 元素,增加了灵活; - 扩容机制:
arraylist
内部的扩容机制能够有效减少扩容次数,提升性能。
缺点:
- 插入和删除效率低:在
arraylist
中间插入或删除元素时,需要移动数组中的其他元素,时间复杂度为 o(n),因此在频繁插入和删除操作的场景中效率较低; - 非线程安全:
arraylist
不是线程安全的,如果在多线程环境中使用,需要手动进行同步,或者使用collections.synchronizedlist
包装; - 占用额外内存:
arraylist
在扩容时需要分配新的数组并复制旧数组中的元素,这会占用额外的内存,并可能导致内存碎片问题; - 空间浪费:当
arraylist
的实际元素数量远小于其容量时,会浪费内存空间; - 扩容时性能开销:当
arraylist
扩容时,需要进行数组复制操作,会导致短暂的性能下降,特别是在大规模扩容时。
适用场景:
- 快速随机访问:如果应用程序需要频繁地通过索引访问元素,
arraylist
是一个很好的选择; - 读取多、写入少:适用于读取操作多于写入操作的场景;
- 存储有序数据:适用于需要保持元素插入顺序的场景。
不适用场景:
- 频繁插入和删除:不适用于频繁在中间位置插入或删除元素的场景,因为这会导致大量的数组移动操作;
- 线程安全要求:不适用于需要线程安全的场景,除非使用同步包装。
2、arraylist 源码
arraylist
通过动态数组来实现,其扩容机制保证了在添加大量元素时能够自动调整容量。它提供了快速的随机访问,但在进行大量插入和删除操作时效率较低,特别是在列表中间进行操作时。这些特性使得 arraylist
适用于需要频繁访问元素但插入和删除操作较少的场景。
为了更好的理解 arraylist
的这些特性,接下来我们以 jdk1.8 中的代码为例,看一下它的源码!
2.1、数据结构
arraylist
通过维护一个数组 elementdata
来存储其元素,并使用两个共享的空数组实例 empty_elementdata
和 defaultcapacity_empty_elementdata
来区分不同状态下的空 arraylist
。当添加第一个元素时,arraylist
会根据当前状态选择合适的扩展策略。这个设计使得 arraylist
能够高效地管理其内部数组,在需要时进行动态扩展。
public class arraylist<e> extends abstractlist<e>
implements list<e>, randomaccess, cloneable, java.io.serializable
{
// 省略其他方法和实现细节
...
/**
* 用于空实例的共享空数组实例。
*/
private static final object[] empty_elementdata = {};
/**
* 用于默认大小空实例的共享空数组实例。
* 我们将其与 empty_elementdata 区分开来,以便在添加第一个元素时知道要扩充多少。
*/
private static final object[] defaultcapacity_empty_elementdata = {};
/**
* 存储 arraylist 元素的数组缓冲区。
* arraylist 的容量即为此数组缓冲区的长度。
* 任何 elementdata == defaultcapacity_empty_elementdata 的空 arraylist 在添加第一个元素时会扩展到 default_capacity。
*/
transient object[] elementdata; // 为简化嵌套类访问,非私有
/**
* arraylist 的大小(它包含的元素数量)。
*
* @serial
*/
private int size;
// 省略其他方法和实现细节
...
}
2.2、构造方法
arraylist
提供了灵活的构造方法来初始化列表,可以指定初始容量、使用默认容量或通过集合初始化。通过这些构造方法,arraylist
能够满足不同场景下的初始化需求,并且在初始化时根据传入参数的不同进行相应的处理和优化。
public class arraylist<e> extends abstractlist<e>
implements list<e>, randomaccess, cloneable, java.io.serializable
{
// 省略其他方法和实现细节
...
/**
* 使用指定的初始容量构造一个空列表。
*
* @param initialcapacity 列表的初始容量
* @throws illegalargumentexception 如果指定的初始容量为负数
*/
public arraylist(int initialcapacity) {
if (initialcapacity > 0) {
this.elementdata = new object[initialcapacity];
} else if (initialcapacity == 0) {
this.elementdata = empty_elementdata;
} else {
throw new illegalargumentexception("illegal capacity: " + initialcapacity);
}
}
/**
* 构造一个初始容量为十的空列表。
*/
public arraylist() {
this.elementdata = defaultcapacity_empty_elementdata;
}
/**
* 构造一个包含指定集合元素的列表,按集合的迭代器返回的顺序。
*
* @param c 包含要放入此列表中的元素的集合
* @throws nullpointerexception 如果指定的集合为 null
*/
public arraylist(collection<? extends e> c) {
object[] a = c.toarray();
if ((size = a.length) != 0) {
if (c.getclass() == arraylist.class) {
elementdata = a;
} else {
elementdata = arrays.copyof(a, size, object[].class);
}
} else {
// 用空数组替换。
elementdata = empty_elementdata;
}
}
// 省略其他方法和实现细节
...
}
2.3、元素访问
正因为 arraylist
的底层实现是数组(一块连续的内存区域),所以可以轻松的实现快速的随机访问和按索引操作。get
方法在访问或修改元素之前都会调用 rangecheck
方法检查索引的有效性,而 rangecheck
方法会在索引超出范围时抛出详细的错误信息。
public class arraylist<e> extends abstractlist<e>
implements list<e>, randomaccess, cloneable, java.io.serializable
{
// 省略其他方法和实现细节
...
/**
* 在指定索引处获取元素。
*
* @param index 要获取元素的位置
* @return 指定索引处的元素
*/
e elementdata(int index) {
return (e) elementdata[index];
}
/**
* 获取指定索引处的元素,并进行范围检查。
*
* @param index 要获取元素的位置
* @return 指定索引处的元素
* @throws indexoutofboundsexception 如果索引超出范围
*/
public e get(int index) {
rangecheck(index); // 检查索引是否在范围内
return elementdata(index); // 返回指定索引处的元素
}
/**
* 检查索引是否在范围内。
*
* @param index 要检查的索引
* @throws indexoutofboundsexception 如果索引超出范围
*/
private void rangecheck(int index) {
if (index >= size || index < 0) {
throw new indexoutofboundsexception(outofboundsmsg(index));
}
}
/**
* 构建索引超出范围的错误信息。
*
* @param index 超出范围的索引
* @return 错误信息字符串
*/
private string outofboundsmsg(int index) {
return "index: " + index + ", size: " + size;
}
// 省略其他方法和实现细节
...
}
2.4、添加操作
我们知道,数组需要使用着一块连续的内存空间,因此数组的大小一旦被规定就无法改变。那如果我们不断的往里面添加数据的话,arraylist
是如何进行扩容的呢 ?答案就是 arraylist
会复制一个新的更大容量的数组:arraylist 在添加元素时会先调用名为 ensurecapacityinternal(int mincapacity)
的方法,对数组容量进行检查,判断剩余空间是否足够,不够时则进行扩容。
public class arraylist<e> extends abstractlist<e>
implements list<e>, randomaccess, cloneable, java.io.serializable
{
// 省略其他方法和实现细节
...
/**
* 在 arraylist 末尾添加一个元素。
*
* @param e 要添加的元素
* @return 总是返回 true
*/
public boolean add(e e) {
ensurecapacityinternal(size + 1); // 确保容量足够容纳新元素,增加 modcount
elementdata[size++] = e; // 将新元素添加到数组中,size 自增
return true;
}
/**
* 确保 arraylist 的容量足够容纳指定数量的元素。
*
* @param mincapacity 需要的最小容量
*/
private void ensurecapacityinternal(int mincapacity) {
ensureexplicitcapacity(calculatecapacity(elementdata, mincapacity));
}
/**
* 计算所需的容量
*
* @param elementdata 当前的数组
* @param mincapacity 需要的最小容量
* @return 计算后的容量
*/
private static int calculatecapacity(object[] elementdata, int mincapacity) {
if (elementdata == defaultcapacity_empty_elementdata) {
return math.max(default_capacity, mincapacity);
}
return mincapacity;
}
/**
* 显式地确保 arraylist 的容量足够。
*
* @param mincapacity 需要的最小容量
*/
private void ensureexplicitcapacity(int mincapacity) {
// 递增修改计数,以支持快速失败机制
modcount++;
// overflow-conscious code
if (mincapacity - elementdata.length > 0)
grow(mincapacity); // 如果所需容量超过当前数组长度,则扩展数组
}
/**
* 扩展数组的容量。
*
* @param mincapacity 需要的最小容量
*/
private void grow(int mincapacity) {
// overflow-conscious code
int oldcapacity = elementdata.length;
int newcapacity = oldcapacity + (oldcapacity >> 1);
if (newcapacity - mincapacity < 0)
newcapacity = mincapacity;
if (newcapacity - max_array_size > 0)
newcapacity = hugecapacity(mincapacity);
elementdata = arrays.copyof(elementdata, newcapacity);
}
// 省略其他方法和实现细节
...
}
2.5、扩容机制
扩容后的数组大小是原大小的 1.5 倍(oldcapacity + (oldcapacity >> 1)
)。最后将旧数组进行复制(调用 arrays.copyof()
,再调用 system.arraycopy()
),达到扩容的目的,此时新旧列表的 size
大小相同,但 elementdata
的长度即容量不同。
public class arraylist<e> extends abstractlist<e>
implements list<e>, randomaccess, cloneable, java.io.serializable
{
// 省略其他方法和实现细节
...
/**
* 扩展数组的容量。
*
* @param mincapacity 需要的最小容量
*/
private void grow(int mincapacity) {
// 将数组长度赋值给 oldcapacity
int oldcapacity = elementdata.length;
// 将 oldcapacity 右移一位再加上 oldcapacity,即相当于 newcapacity=1.5 倍 oldcapacity(不考虑精度损失)
int newcapacity = oldcapacity + (oldcapacity >> 1);
// 如果 newcapacity 还是小于 mincapacity,直接将 mincapacity 赋值给 newcapacity
if (newcapacity - mincapacity < 0)
newcapacity = mincapacity;
// 特殊情况:newcapacity 的值过大,直接将整型最大值赋给 newcapacity,
// 即 newcapacity=integer.max_value
if (newcapacity - max_array_size > 0)
newcapacity = hugecapacity(mincapacity);
// 将 elementdata 的数据拷贝到扩容后的数组
elementdata = arrays.copyof(elementdata, newcapacity);
}
/`
* 如果大于临界值,进行整型最大值的分配
*
* @param mincapacity 需要的最小容量
*/
private static int hugecapacity(int mincapacity) {
if (mincapacity < 0) {
// overflow
throw new outofmemoryerror();
}
return (mincapacity > max_array_size) ? integer.max_value : max_array_size;
}
// 省略其他方法和实现细节
...
}
2.6、删除操作
arraylist 在进行根据索引删除时,通过系统 native
方法 system.arraycopy
完成的,原理就是将删除位置后的所有元素重新复制到删除位置到原来最后元素的前一位,并使用 elementdata[--size] = null;
清空多余的值。
package java.util;
import ...
public class arraylist<e> extends abstractlist<e>
implements list<e>, randomaccess, cloneable, java.io.serializable
{
// 省略其他方法和实现细节
...
/**
* 从列表中移除指定位置的元素。
* 将所有后续元素左移(索引减一)。
*
* @param index 要移除元素的索引位置
* @return 被移除的元素
* @throws indexoutofboundsexception 如果索引超出范围抛出此异常
*/
public e remove(int index) {
rangecheck(index); // 检查索引是否在合法范围内,防止索引越界
modcount++; // 修改次数加一,用于实现迭代器的快速失败机制(在迭代过程中如果检测到结构变化会抛出异常)
e oldvalue = elementdata(index); // 获取并保存将要被移除的元素值
int nummoved = size - index - 1; // 计算从 index 之后需要左移的元素数量
if (nummoved > 0)
system.arraycopy(elementdata, index + 1, elementdata, index,
nummoved); // 使用系统方法 arraycopy 来高效地移动数组元素
elementdata[--size] = null; // 将最后一个元素设为 null,帮助垃圾收集器回收,并减少实际元素数量
return oldvalue; // 返回被移除的元素
}
/**
* 从列表中移除第一次出现的指定元素(如果存在)。
* 如果列表不包含该元素,则不做改变。
* 更正式地说,移除满足 (o==null ? get(i)==null : o.equals(get(i))) 条件的最低索引的元素。
*
* @param o 要从列表中移除的元素
* @return 如果列表包含指定元素则返回 true
*/
public boolean remove(object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementdata[index] == null) {
fastremove(index); // 快速移除方法,不进行返回值操作
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementdata[index])) {
fastremove(index); // 快速移除方法,不进行返回值操作
return true;
}
}
return false; // 如果没有找到元素,返回 false
}
/**
* 一个私有的快速移除方法,不进行边界检查也不返回被移除的值。
* 用于内部操作,以提高性能。
*/
private void fastremove(int index) {
modcount++; // 修改计数器增加
int nummoved = size - index - 1; // 计算需要移动的元素数量
if (nummoved > 0)
system.arraycopy(elementdata, index + 1, elementdata, index,
nummoved); // 高效地移动数组元素
elementdata[--size] = null; // 清理引用,帮助垃圾回收
}
// 省略其他方法和实现细节
...
}
3、arraylist 知识点补充
3.1、arraylist 方法概述
arraylist
类提供了多种方法,用于元素的添加、删除、访问、修改等操作。以下是 arraylist
类的方法列表,并标注了每个方法的来源(自带、接口、父类)和参数说明。
add(e e)
:实现 (list),添加元素到列表末尾,返回值类型为boolean
;add(int index, e element)
:实现 (list),在指定位置插入元素,无返回值;addall(collection<? extends e> c)
:实现 (list),添加集合中的所有元素,返回值类型为boolean
;addall(int index, collection<? extends e> c)
:实现 (list),在指定位置插入集合中的所有元素,返回值类型为boolean
;clear()
:自带,清空列表,无返回值;clone()
:自带,克隆列表,返回值类型为object
;contains(object o)
:实现 (collection),检查列表是否包含指定元素,返回值类型为boolean
;ensurecapacity(int mincapacity)
:自带,确保列表的最小容量,无返回值;foreach(consumer<? super e> action)
:实现 (iterable),对列表中的每个元素执行指定操作,无返回值;get(int index)
:自带,获取指定位置的元素,返回值类型为e
;indexof(object o)
:自带,返回指定元素第一次出现的位置,返回值类型为int
;isempty()
:实现 (collection),检查列表是否为空,返回值类型为boolean
;iterator()
:实现 (iterable),返回列表的迭代器,返回值类型为iterator<e>
;lastindexof(object o)
:自带,返回指定元素最后一次出现的位置,返回值类型为int
;listiterator()
:自带,返回列表的列表迭代器,返回值类型为listiterator<e>
;listiterator(int index)
:自带,返回从指定位置开始的列表迭代器,返回值类型为listiterator<e>
;remove(int index)
:自带,移除指定位置的元素,返回值类型为e
;remove(object o)
:自带,移除首次出现的指定元素,返回值类型为boolean
;removeall(collection<?> c)
:自带,移除所有包含在指定集合中的元素,返回值类型为boolean
;removeif(predicate<? super e> filter)
:实现 (collection),移除满足条件的元素,返回值类型为boolean
;replaceall(unaryoperator<e> operator)
:实现 (list),替换每个元素,无返回值;retainall(collection<?> c)
:自带,保留所有包含在指定集合中的元素,返回值类型为boolean
;set(int index, e element)
:自带,替换指定位置的元素,返回值类型为e
;size()
:自带,返回列表的大小,返回值类型为int
;sort(comparator<? super e> c)
:自带,排序列表,无返回值;spliterator()
:实现 (collection),返回列表的可拆分迭代器,返回值类型为spliterator<e>
;sublist(int fromindex, int toindex)
:自带,返回指定范围的子列表,返回值类型为list<e>
;toarray()
:实现 (collection),将列表转换为数组,返回值类型为object[]
;toarray(t[] a)
:实现 (collection),将列表转换为指定类型的数组,返回值类型为t[]
;trimtosize()
:自带,修剪列表的容量为当前大小,无返回值。
3.2、在遍历 arraylist 时移除元素时的问题
在遍历 arraylist
或其他类型的 list
集合时进行元素的删除,确实需要格外小心。这是因为集合的结构修改(如添加或删除元素)可能会影响遍历过程,导致 concurrentmodificationexception
异常或者跳过元素。这里详细解释下这两种情况:
3.2.1、concurrentmodificationexception
concurrentmodificationexception
是 java 在迭代器检测到除了它自身的 remove()
方法之外的任何修改时抛出的异常。这是 java 集合框架的 “fail-fast” 行为的一部分,用来快速响应错误,防止未定义的行为。
当你使用迭代器或者增强型 for 循环(本质上使用的是迭代器)遍历 arraylist
时,如果在遍历过程中通过集合的直接方法(如 list.remove(index)
)修改集合,就会导致迭代器的内部状态与集合的状态不一致。因为迭代器内部有一个 expectedmodcount
变量,用来记录初始化迭代器时集合的修改次数。每次迭代时,迭代器都会检查集合的当前修改次数是否仍然等于 expectedmodcount
。如果不等,就会抛出 concurrentmodificationexception
。
public class main {
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);
for (integer number : list) {
if (number % 2 == 0) {
list.remove(number); // 尝试删除元素
}
}
}
}
在这个例子中,尝试删除所有偶数。因为 list.remove(object)
修改了集合,而这种修改没有通过迭代器进行,所以当迭代器检测到修改时(在下一次调用 next()
时),它将抛出 concurrentmodificationexception
3.2.2、跳过元素
另一个常见的问题是在使用普通的 for 循环遍历并同时删除元素时可能会跳过某些元素。这是因为删除元素会影响数组的索引:
import java.util.arraylist;
import java.util.list;
public class main {
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);
for (int i = 0; i < list.size(); i++) {
if (list.get(i) % 2 == 0) {
list.remove(i); // 删除元素
i--; // 通过递减 i 来调整索引
}
}
}
}
在这个例子中,我们尝试删除所有偶数。如果不递减 i
的值,当元素被移除时,列表中的元素会向左移动,这导致下一个元素跳过检查。在上面的代码中,通过 i--
调整,我们倒是可以确保即使删除了元素,也不会跳过任何元素。
3.3、遍历 arraylist 时移除元素时的问题解决方案
3.3.1、使用迭代器的 remove 方法
当使用迭代器直接遍历时,应该使用迭代器自身的 remove()
方法来删除元素。这样可以安全地更新 modcount
和 expectedmodcount
,防止 concurrentmodificationexception
:
iterator<integer> it = list.iterator();
while (it.hasnext()) {
integer value = it.next();
if (value % 2 == 0) {
it.remove(); // 安全删除
}
}
3.3.2、使用 java8 的 removeif 方法
java8 提供的 removeif()
方法可以在内部安全地修改列表,并避免 concurrentmodificationexception
:
list.removeif(n -> n % 2 == 0); // 删除所有偶数
3.3.3、使用逆向遍历
使用传统的 for
循环时,从列表的尾部向前遍历和删除可以避免跳过元素的问题:
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i) % 2 == 0) {
list.remove(i);
}
}
这些方法各有优势和适用场景。选择哪种方法取决于具体的需求和环境。在多数情况下,使用迭代器的 remove()
方法或 removeif()
方法提供了最简洁和安全的解决方案。
发表评论