ArrayList 实现原理
一、 ArrayList 概述
ArrayList 是基于数组实现的,是一个动态数组,其容量能自动增长,类似于 C 语言中的动态申请内存,动态增长内存。ArrayList 不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用 Collections.synchronizedList(List l)函数返回一个线程安全的 ArrayList 类,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
ArrayList 实现了 Serializable 接口,因此它支持序列化,能够通过序列化传输,实现了 RandomAccess 接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了 Cloneable 接口,能被克隆。
每个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造 ArrayList 时指定其容量。在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。
注意,此实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。
二、ArrayList 的实现
对于 ArrayList 而言,它实现 List 接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析 ArrayList 的源代码:
私有属性
ArrayList 定义只定义类两个私有属性:
// 声明数组
private transient Object[] elementData;
// ArrayList容量
private int size;
elementData 存储 ArrayList 内的元素,size 表示它包含的元素的数量
有个关键字需要解释:transient。 Java 的 serialization 提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的一个域上关闭 serialization,可以在这个域前加上关键字 transient。如下:
public class UserInfo implements Serializable {
private static final long serialVersionUID = 996890129747019948L;
private String name;
private transient String psw;
public UserInfo(String name, String psw) {
this.name = name;
this.psw = psw;
}
public String toString() {
return "name=" + name + ", psw=" + psw;
}
}
public class TestTransient {
public static void main(String[] args) {
UserInfo userInfo = new UserInfo("张三", "123456");
System.out.println(userInfo);
try {
// 序列化,被设置为transient的属性没有被序列化
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
"UserInfo.out"));
o.writeObject(userInfo);
o.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
try {
// 重新读取内容
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
"UserInfo.out"));
UserInfo readUserInfo = (UserInfo) in.readObject();
//读取后psw的内容为null
System.out.println(readUserInfo.toString());
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
被标记为 transient 的属性在对象被序列化的时候不会被保存
构造方法
ArrayList 提供了三种方式的构造器,可以构造一个默认初始容量为 10 的空列表、构造一个指定初始容量的空列表以及构造一个包含指定 collection 的元素的列表,这些元素按照该 collection 的迭代器返回它们的顺序排列的:
// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 新建一个数组
this.elementData = new Object[initialCapacity];
}
// ArrayList无参构造函数。默认容量是10。
public ArrayList() {
this(10);
}
// 创建一个包含collection的ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
元素存储
ArrayList 提供了 set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加元素的方法。下面我们一一讲解:
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
// 将指定的元素插入此列表中的指定位置。
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
// 如果数组长度不足,将进行扩容。
ensureCapacity(size+1); // Increments modCount!!
// 将 elementData中从Index位置开始、长度为size-index的元素,
// 拷贝到从下标为index+1位置开始的新的elementData数组中。
// 即将当前位于该位置的元素以及所有后续元素右移一个位置。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
元素读取
// 返回此列表中指定位置上的元素。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
元素删除
ArrayList 提供了根据下标或者指定对象两种方式的删除功能。如下:
// 移除此列表中指定位置上的元素。
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
首先是检查范围,修改 modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将 list 末尾元素置空(null),返回被移除的元素。
// 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。
public boolean remove(Object o) {
// 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 类似remove(int index),移除列表中指定位置上的元素。
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
}
首先通过代码可以看到,当移除成功后返回 true,否则返回 false。remove(Object o)中通过遍历 element 寻找是否存在传入对象,一旦找到就调用 fastRemove 移除对象。为什么找到了元素就知道了 index,不通过 remove(index)来移除元素呢?因为 fastRemove 跳过了判断边界的处理,因为找到元素就相当于确定了 index 不会超过边界,而且 fastRemove 并不返回被移除的元素。下面是 fastRemove 的代码,基本和 remove(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; // Let gc do its work
}
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// Let gc do its work
int newSize = size - (toIndex-fromIndex);
while (size != newSize)
elementData[--size] = null;
}
执行过程是将 elementData 从 toIndex 位置开始的元素向前移动到 fromIndex,然后将 toIndex 位置之后的元素全部置空顺便修改 size。
调整数组容量
从上面介绍的向 ArrayList 中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法 ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用 ensureCapacity 来手动增加 ArrayList 实例的容量,以减少递增式再分配的数量。
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1; //增加50%+1
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的 1.5 倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用 ensureCapacity 方法来手动增加 ArrayList 实例的容量。
Object oldData[] = elementData;//为什么要用到 oldData[]
乍一看来后面并没有用到关于 oldData, 这句话显得多此一举!但是这是一个牵涉到内存管理的类, 所以要了解内部的问题。 而且为什么这一句还在 if 的内部,这跟 elementData = Arrays.copyOf(elementData, newCapacity); 这句是有关系的,下面这句 Arrays.copyOf 的实现时新创建了 newCapacity 大小的内存,然后把老的 elementData 放入。好像也没有用到 oldData,有什么问题呢。问题就在于旧的内存的引用是 elementData, elementData 指向了新的内存块,如果有一个局部变量 oldData 变量引用旧的内存块的话,在 copy 的过程中就会比较安全,因为这样证明这块老的内存依然有引用,分配内存的时候就不会被侵占掉,然后 copy 完成后这个局部变量的生命期也过去了,然后释放才是安全的。不然在 copy 的的时候万一新的内存或其他线程的分配内存侵占了这块老的内存,而 copy 还没有结束,这将是个严重的事情。
关于 ArrayList 和 Vector 区别如下:
- ArrayList 在内存不够时默认是扩展 50% + 1 个,Vector 是默认扩展 1 倍。
- Vector 提供 indexOf(obj, start)接口,ArrayList 没有。
- Vector 属于线程安全级别的,但是大多数情况下不使用 Vector,因为线程安全需要更大的系统开销。
ArrayList 还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过 trimToSize 方法来实现。代码如下:
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
由于 elementData 的长度会被拓展,size 标记的是其中包含的元素的个数。所以会出现 size 很小但 elementData.length 很大的情况,将出现空间的浪费。trimToSize 将返回一个新的数组给 elementData,元素内容保持不变,length 和 size 相同,节省空间。
转为静态数组 toArray
注意 ArrayList 的两个转化为静态数组的 toArray 方法。
第一个, 调用 Arrays.copyOf 将返回一个数组,数组内容是 size 个 elementData 的元素,即拷贝 elementData 从 0 至 size-1 位置的元素到新数组并返回。
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
第二个,如果传入数组的长度小于 size,返回一个新的数组,大小为 size,类型与传入数组相同。所传入数组长度与 size 相等,则将 elementData 复制到传入数组中并返回传入的数组。若传入数组长度大于 size,除了复制 elementData 外,还将把返回数组的第 size 个元素置为空。
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
三、总结
关于 ArrayList 的源码,给出几点比较重要的总结:
1、注意其三个不同的构造方法。无参构造方法构造的 ArrayList 的容量默认为 10,带有 Collection 参数的构造方法,将 Collection 转化为数组赋给 ArrayList 的实现数组 elementData。
2、注意扩充容量的方法 ensureCapacity。ArrayList 在每次增加元素(可能是 1 个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的 1.5 倍加 1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用 Arrays.copyof()方法将元素拷贝到新的数组(详见下面的第 3 点)。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用 ArrayList,否则建议使用 LinkedList。
3、ArrayList 的实现中大量地调用了 Arrays.copyof()和 System.arraycopy()方法。我们有必要对这两个方法的实现做下深入的了解。
首先来看 Arrays.copyof()方法。它有很多个重载的方法,但实现思路都是一样的,我们来看泛型版本的源码:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
很明显调用了另一个 copyof 方法,该方法有三个参数,最后一个参数指明要转换的数据的类型,其源码如下:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
这里可以很明显地看出,该方法实际上是在其内部又创建了一个长度为 newlength 的数组,调用 System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。
下面来看 System.arraycopy()方法。该方法被标记了 native,调用了系统的 C/C++ 代码,在 JDK 中是看不到的,但在 openJDK 中可以看到其源码。该函数实际上最终调用了 C 语言的 memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java 强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
4、ArrayList 基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。
5、在查找给定元素索引值等的方法中,源码都将该元素的值分为 null 和不为 null 两种情况处理,ArrayList 中允许元素为 null。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于