ArrayList,在我们写 Android 的时候经常配合 ListView ,RecyclerView,ViewPager 使用。那么今天就来解析一下它。
创建
构造函数
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
我们可以发现,默认的构造函数中就只有一句话。但也表明了自己的真实身份,就是一个控制一个 Object 数组的类。
但是,为什么还要给 elementData 赋值一个静态的Object数组呢?为什么不直接 new 一个Object 数组呢?我们知道,static final 修饰的常量存在于内存中的方法区,且有且仅有一个实例,所以当我们再次创建一个 ArrayList 的对象时,elementData 指向的还是同一个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
由图可以看出,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在这里充当的是一个缓冲区的作用,避免创建多个无用的数组。666
扩容
在上一节中我们可以发现 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的长度是 0 的,那还怎么存储数据呢?而且,难道程序中所有的数据都是存储在同一个静态数组中吗?带着疑问,我们来看一下 add()方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
我们可以看到第二句很明显就是插入数据并使长度自增的操作,那么第一句的方法 ensureCapacityInternal() 应该就是扩容的操作了。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
我们首先对 elementData 进行了判断,若相等则比较 10 与 var1 的大小,接着执行下一个方法。看到这里,我们可以猜测,ArrayList 在最初应该是直接扩增到了 10 。但是,为什么要加一个判断呢,是因为 elementData 的指向会改变吗?我们继续看下一个方法:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
该方法,先使 modCount 自增,表示修改次数增加。然后判断当前传入的值是否比当前数组长度大,若是则执行 grow()方法
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);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
grow()方法中,前面的都是对数组大小做一些限制,不多赘述。最后一行:
this.elementData = Arrays.copyOf(this.elementData, newCapacity);
这一行,应该就是做扩容操作了,Arrays.copyOf()的参数列表中,分别是,elementData 和 newCapacity。我们回过头:
int newCapacity = oldCapacity + (oldCapacity >> 1);
结合前面的猜想,每次扩容应该都是扩大了1.5倍的。继续深入:
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
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;
}
在这里,利用传入的数据创建了一个新的数组,然后把原本数组的数据复制到了新的数组对象中并返回。现在,我们不仅证实了前面的猜想,疑问也都解决了。我们试着继续深入
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
看到 native ,所以应该是用到了 c 或 c++ 的方法。但是源码看不到了 = =;
System.arraycopy()
通过查阅资料,我发现其实 System.arraycopy() 内部实现是用 c 语言中的 memmove()/memcpy() 函数来实现的。它们是一个内存操作函数,通过直接移动指针来实现数组的复制。更多内容,我也还不是很清楚,希望高手指点。
扩容总结
- ArrayList 对象创建后,内部数组的初始长度未0
- 首次添加数据时,会扩容到10
- 每次扩容,都会扩大原来的1.5倍
- 每次扩容,实际都是新创建一个数组然后再把数据复制到新数组中然后返回
- size 表示的是逻辑容量而不是内部数组的实际容量
常用操作解析
get
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
我们清楚,数组只要知道下标就能直接获取数据。ArrayList 也是如此,先检查是否越界,然后直接通过下标获取数据。
set
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
同样修改也一样,时间复杂度与获取一样 同为 O(1)
remove
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(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; // clear to let GC do its work
return oldValue;
}
算出需要移动的距离后,直接利用 System.arraycopy 把后面的参数移动到前面,最后在制空最后一位。
add && addAll
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
public boolean addAll(Collection<? extends E> var1) {
Object[] var2 = var1.toArray();
int var3 = var2.length;
this.ensureCapacityInternal(this.size + var3);
System.arraycopy(var2, 0, this.elementData, this.size, var3);
this.size += var3;
return var3 != 0;
}
两个方法都很相似,扩容后,利用 System.arraycopy 进行复制。
clear
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
简单粗暴,直接全部置空。
SubList
在查看源码的时候还发现了 ArrayList 中,还存在一个内部类 SubList,他与 ArrayList 一样都是继承 Abstractlist 。同时也包含get , set , add ,remove 等方法。
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public void add(int index, E e) {
if (index < 0 || index > this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
.
.
.
.
.
.
}
我们发现,parent 即是原始的 list 的引用,在其他对数组操作的方法中,最后也都是调用了 parent 中的方法。即,对 subList 的操作其实就是对 ArrayList 的操作,会改变 ArrayList 的内容。
SubList 的妙用
list.subList(100,200).clear();
直接删除一段数据(100-199),而不用 for 循环。
SubList 的缺陷
创建 subList 后,如果修改了 ArrayList ,再次调用 SubList 的时候,会发生异常
List<String> stringList = new ArrayList<>();
stringList.add("1");
stringList.add("2");
stringList.add("3");
stringList.add("4");
stringList.add("5");
List<String> subList = stringList.subList(0,stringList.size());
stringList.add("6");
System.out.print("stringList = "+stringList.size());
System.out.print("subList = "+subList.size());
异常
Exception in thread "main" java.util.ConcurrentModificationException
stringList = 6 at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
at java.util.ArrayList$SubList.size(ArrayList.java:1040)
at com.jinjunhuang.arraylistdemo.Demo.main(Demo.java:32)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
至于原因,我们来看一下 size()方法
public int size() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
return this.size;
}
当修改了 ArrayList 后,ArrayList.modeCount 会改变,而 sublist 的 modCount 只在构造函数中获取 ArrayList.modeCount 的值,而不会动态改变,所以,在执行 sublist 的方法时,两者不相等,就会抛出错误。这种机制也叫 fail-fast。
fail-fast 机制
是Java集合的一种检测错误机制。当多个线程对集合进行机构上的改变的操作时,有可能会产生 fail-fast 机制。这种机制,不会保证一定会出现错误,但会进最大努力去抛出 ConcurrentModificationException 异常。迭代器中也存在这种机制。获取更多信息推荐阅读
序列化
在谈到构造函数的时候,我们会发现,ArrayList 中的 Object 数组中用了 transient 来修饰,表示不用序列化,但是我们的数据都是放在数组中的,数据不序列化,我们持久化了有什么用呢?这是因为,elementDa 不一定是满的,没有用上的空间我们并不想再耗时间去序列化。但有存储数据的空间我们还是要序列化的,因此,Java 中重写了 writeObject 方法,以实现局部序列化。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
总结
- ArrayList 其实
- 就是维护一个一维数组的对象
- 每次创建 ArrayList 对象,内部的数组引用都会指向一个长度为0的数组常量
- 扩容就是新建一个更大的数组对象,然后利用 System.arraycopy() 复制旧数组中的数据
- 首次扩容会扩张到 10
- 每次扩容都会扩张到原来的 1.5倍
- ArrayList 和数组一样,获取数据很快速,但是插入删除会很慢
- 插入删除操作也是利用 System.arraycopy()来实现数据的移动的
- System.arraycopy()是采用 c 语言的 memcpy/memmove 实现的
- ArrayList 中 SubList 是 ArrayList 中的一个视图,修改 SubList ,ArrayList 中的数据也会改变
- 若创建了 SubList 后修改了 ArrayList ,再次调用前面创建的 SubList 的实例,会触发 fail-fast机制的发生
- ArrayList 中重写了 writeObject 方法以优化数组的序列化过程