Java 集合学习笔记:ArrayList
Posted 笑虾
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合学习笔记:ArrayList相关的知识,希望对你有一定的参考价值。
Java 集合学习笔记:ArrayList
简介
ArrayList 是 List 接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于 Vector 类,除了此类是非线程安全的。)
List
的 数组
实现。
- 因为底层数据解构是
数组
,所以特点是连续内存空间,根据索引操作。 - 优势在读,频繁写操作相对吃亏。有两个性能消耗点:
2.1. 插入/删除时,当前元素之后的所有元素需要整体平移。
2.2. 空间不足时的反复自动扩容。
UML
从类图可见ArrayList
并不是直接实现 List
。
ArrayList
是继承 AbstractList
再进行扩展的。
至于实现List
接口的行为,听说是失误,反正删不删也不影响,所以就一直没动。
常用方法
增
方法 | 说明 |
---|---|
boolean add (E e) | 在列表末尾添加新元素。 |
void add (int index, E element) | 在指定位置添加元素,原 index 到末尾的所有元素,整体后移一位。 |
boolean addAll (Collection<? extends E> c) | 将指定集合添加 到当前列表的末尾 。 |
boolean addAll (int index, Collection<? extends E> c) | 将指定集合插入 当前列表的指定位置 。 |
删
方法 | 说明 |
---|---|
void clear () | 清空列表。 |
E remove (int index) | 移除指定索引上的元素。右侧所有元素整体向左平移一位。 |
boolean remove (Object o) | 移除指定元素。如果有多个,每次只会移除第一个。 |
boolean removeAll (Collection<?> c) | 删除指定集合中包含的元素。 |
boolean removeIf (Predicate<? super E> filter) | 删除复合条件的元素。(filter 是一个返回布尔型的 Lambda) |
boolean retainAll (Collection<?> c) | 保留当前列表 与目标集合 中都存在 的元素。也就是,从当前列表中删除指定集合中不存在的所有元素。 |
改
方法 | 说明 |
---|---|
E set (int index, E element) | 替换指定位置的元素 |
void replaceAll (UnaryOperator operator) | 遍历列表执行特定的操作,实现替换。list.replaceAll(x -> x * 2); |
void sort (Comparator<? super E> c) | 传入一个比较器。list.sort((a,b) -> b-a); |
查
方法 | 说明 |
---|---|
size () | 获取列表大小。 |
boolean isEmpty () | 判断列表是否为空。 |
E get (int index) | 按索引获取元素。 |
boolean contains (Object o) | 判断列表是否包含指定元素。 |
int indexOf (Object o) | 返回指定元素在列表中的索引位置 。 |
int lastIndexOf (Object o) | 返回指定元素最后一次出现的位置。 |
迭代
方法 | 说明 |
---|---|
void forEach (Consumer<? super E> action) | 遍历列表执行消费者。 |
Iterator iterator () | 返回迭代器 |
Spliterator spliterator () | 返回可拆分迭代器。 |
ListIterator listIterator () | 返回双向迭代器。 |
ListIterator listIterator (int index) | 从指定的位置开始,返回双向迭代器。 |
List subList (int fromIndex, int toIndex) | 返回指定范围的subList |
Object[] toArray () | 返回 Object 数组 |
<T> T[] toArray (T[] a) | 返回指定类型数组。 list.toArray(new Integer[0]); |
内部类
类 | 说明 |
---|---|
class Itr implements Iterator | 实现了 Iterator 接口,iterator() 返回的就是它。 |
class ListItr extends Itr implements ListIterator | 双向迭代器。listIterator() 返回的就是它。 |
private class SubList extends AbstractList implements RandomAccess | 私有内部类。subList() 返回的就是它。 |
静态内部类
类 | 说明 |
---|---|
static final class ArrayListSpliterator<E> implements Spliterator<E> | 实现了可拆分迭代器接口。 |
自动扩容逻辑
Java7
– | 说明 |
---|---|
Object[] elementData; | 被封装的数组对象 :ArrayList 内部真正储存数据的容器。自动扩容其实就是在折腾 elementData 。 |
int size; | 元素个数: ArrayList 中实际存储元素的个数。 |
DEFAULT_CAPACITY = 10; | 默认初始容量 |
Object[] EMPTY_ELEMENTDATA = ; | 初始化容器 :所有ArrayList 都用它初始化,避免创建无数个空数组浪费。 |
默认扩容因子 | oldCapacity + (oldCapacity >> 1) : 原长度 x 1.5 |
扩容方式 | 1. 调用 ensureCapacity(int minCapacity) 手动扩容。2. add 时自动检测并按需扩容。3. addAll 时自动检测并按需扩容。4. readObject(ObjectInputStream s) 时自动检测并按需扩容。 |
minCapacity
:最小所需容积
newCapacity
:新容积
- 计算新容积
newCapacity
:
1.1. 默认扩容1.5倍。
1.2. 如果1.5不够,则扩容到所需大小minCapacity
。
1.3. 如果新容积newCapacity
>MAX_ARRAY_SIZE
则进一步判断:(minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
- 底层调用
Arrays.copyOf(elementData, newCapacity)
创建新数组,再将结果
赋给原容器elementData
。
2.1.Arrays.copyOf
的底层是native
原生方法System.arraycopy
Java8
- | 说明 |
---|---|
int DEFAULT_CAPACITY = 10; | 默认初始容量。 |
Object[] EMPTY_ELEMENTDATA = ; | 列表大小为 0 时共享此空数组。避免每个空列表都创建一个空数组,浪费。 |
Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ; | 1. 初始列表时,没给默认大小,用此空数组。 2. 扩容时用于作为判断条件之一。 |
Object[] elementData; | 内部容器,底层实际用于保存数据的数组。 |
size | 容器当前大小。 |
默认扩容因子 | 新长度 = 旧长度 + (旧长度 >> 1) = 原长度 x 1.5 |
具体的直接看源码吧。。。
扩容 - 核心代码
/**
* 在此列表中的指定位置插入指定元素。
* 将当前位于该位置的元素(如果有)和任何后续元素向右移动(将其索引加一)。
*/
public void add(int index, E element)
// 检查 index 如果越界就抛异常。
rangeCheckForAdd(index);
// 判断内部数组 elementData 大小,如果有需要就扩容。同时列表修改次数 modCount++
ensureCapacityInternal(size + 1);
// 将目标索引后面的所有元素整体向后移一位。用的原生方法 System.arraycopy
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 当前索引位置赋值
elementData[index] = element;
// 列表大小+1
size++;
/**
* 计算内部数组 elementData 最小所需长度
*/
private void ensureCapacityInternal(int minCapacity)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
// 判断内部数组 elementData 大小,如果有需要就扩容。
ensureExplicitCapacity(minCapacity);
/**
* 判断内部数组 elementData 大小,如果有需要就扩容。
*/
private void ensureExplicitCapacity(int minCapacity)
// 列表修改次数 modCount++
modCount++;
// 最小所需长度 > 数组原长度则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
/**
* 扩容,以确保至少要能下 minCapacity 个元素。
*/
private void grow(int minCapacity)
// 原长度
int oldCapacity = elementData.length;
// 新长度 = 原长度 + (原长度 / 2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 新长度够用就用新长度,不够就按 minCapacity 来。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 新长度大于最大限制,就使用最大限制。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 原数组 = 复制原数组中的数据,并按 newCapacity 创建的新数组。
elementData = Arrays.copyOf(elementData, newCapacity);
移除 - 核心代码
E remove(int index)
public E remove(int index)
rangeCheck(index); // 检查索引,越界就抛异常
modCount++; // 列表修改次数 +1
E oldValue = elementData(index); // 缓存当前索引值,删除完成后 return 出去
// 利用 arraycopy 将索引后的全部元素整体前移一位,覆盖当前索引值,达到删除目的。
int numMoved = size - index - 1; // 计算需要整体向左移动的元素个数
// 如果有,就复制(以复制实现移动效果)
// 将数组 elementData 中 index+1 位置开始的元素,复制到 index 处。(也就是向前移一位)
// 需要复制的元素个数就是 numMoved (也就是被删除的元素右侧的所有元素)
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 所有数据复制后向左移一位粘贴,最右侧一格现在就需要销毁了。
elementData[--size] = null; // --size 实现长度减1。同时也将最后一个元素=null 以便垃圾回收。
// 返回被删除的值
return oldValue;
boolean remove(Object o)
// 遍历数组逐个比对,找到第一个复合的元素就移除。
// 唯一区别就在于 null 和普通对象做了区别处理
public boolean remove(Object o)
if (o == null)
for (int index = 0; index < size; index++)
// null 直接使用 == 对比
if (elementData[index] == null)
fastRemove(index);
return true;
else
for (int index = 0; index < size; index++)
// 其他对象使用 equals 对比
if (o.equals(elementData[index]))
fastRemove(index);
return true;
return false;
// 在 remove(int index) 基础上做了简化,省去了对【索引的校验】和【删除值的返回】。
// 因为这里是通过对比对象得出的 index 绝对是存在的。
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; // clear to let GC do its work
根据 remove 我们可以看到,每次执行只会移除第一个匹配到的元素。
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, null, 3, 4, 5, null));
list.remove(null);
System.out.println(list);
// [1, 2, 3, 4, 5, null]
boolean removeAll(Collection<?> c)
public boolean removeAll(Collection<?> c)
Objects.requireNonNull(c); // 如果 c 为 null 抛异常
return batchRemove(c, false); //
/**
* 批量移除
* @param c collection 从当前列表中的元素,如果 c 也有,那么根据 complement 决定去留。
* @param complement 判断对 c 中包含的元素 true=保留,false=移除
*/
private boolean batchRemove(Collection<?> c, boolean complement)
final Object[] elementData = this.elementData;
// r 表示被检测的索引。用于遍历原数组,从 0 一直加到 size-1,逐个检测元素。
// w 表示用于保存的位置。配合 r 每判定保留(移动的元素就是要保留下来的)一个元素 +1。最终 w 也就是最后一个留存元素的索引。
int r = 0, w = 0;
boolean modified = false;
try
// 本质上是两层循环:第一层 for 遍历 elementData
// 第二层用 c.contains 进行了简化。判断 c 中是否包含 elementData的当前元素。
// 如果不包含,就是要保留的元素。将元素从 r 移动到 w。然后 w 向后移一位。
// 否则遇到需要移除的元素时,只需要跳过移动操作。r++ 但 w 没动,下个元素移过就实现了。
// 假设第一个元素就是要移除的,那么移动就是:1到0, 2到1,3到2,以此类推,从第二个元素开始整体向前平移一位。
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
// 将要保留的元素从 r 移到 w
elementData[w++] = elementData[r];
finally
// r 没走到 size 只有一种可能就是异常了。(暂时还没想到什么情况能触发异常)
// 后面的不处理的,没移动的都一把移过来。
// 最后更新 w = w + 刚才这波移动的个数
if (r != size)
System.arraycopy(elementData, r, elementData, w, size - r);
w += size - r;
// w 之后都是要移除的,全部置空,等GC回收
if (w != size)
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
return modified;
使用建议
- 插入:如果插入多个连续的元素,可以使用
addAll
优化 。只需要整体平移一次。 - 扩容:如果能提前确定所需容积,可以手动指定大小来实现优化。省掉多次扩容浪费时间。
- 删除:如是删除连续的几个元素可以使用
removeRange(int fromIndex, int toIndex)
,底层只需要arraycopy
整体平移一次。
3.1. 当然这个方法没有直接开放给用户使用。可以list.subList(3, 5).clear();
间接调它。
3.2.subList.clear()
>List.clear()
>AbstractList.clear()
>>AbstractList.removeRange()
>ArrayList.removeRange()
大概就是这么兜了一圈,实现的。 - 遍历删除:遍历中要删除元素的场景,请用迭代器或Java8的
removeIf
。详情见:笑虾:forEach 遍历中 remove 的BUG,及 Java8 的新推荐。 - 效率:底层的数据结构为数组,基于索引操作。相对来说:查询效率高,增删效率低。
参考资料
笑虾:Java 集合学习笔记:Iterator
笑虾:Java 集合学习笔记:Collection
以上是关于Java 集合学习笔记:ArrayList的主要内容,如果未能解决你的问题,请参考以下文章