ArrayList介绍
ArrayList是我们使用的最多的集合类了。特点主要如下:
- 基于数组实现底层存储,容量可以自动扩展。
- 实现List全部接口。
- 允许null对象
- 提供操作数组容量大小的方法。
- 粗略来看和Vector类相同,但是 Vector在 方法上加了同步,所以Vector是线程安全的。
- size,isEmpty,get,set,iterator和listIterator运行时间为常量。
- add方法 如果添加n个元素 时间复杂度O(n)
- ArrayList非 thradsafe,所以自己wrap或者用Collections#synchronizedList
- 返回的iterator是fail-fast,如果iterator创建后,修改了list结构则会抛出异常ConcurrentModificationException,
注意这里异常表明是一个bug,需要避免,而不能处理业务逻辑。
size 变量
size 表示当前list中存储了元素的个数,初始化为0。add一个元素加一,删除一个元素减一。
capacity(容量)增长策略
每个ArrayList示例都有一个capacity。表示数组(list中用来储存元素)的大小。
capacity至少和size相等,不会小于它。capacity大小是自动增长的。你也可以调用ArrayList#ensureCapacity方法改变容量。
不过如果你将容量改小 是不起作用的,ensureCapacity方法不允许。
增长容量的方法有三个。
ensureCapacity
` /**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}`
说明:
- public的,说明是提供给用户操作的,因为其他两个方法是private的。这个方法Arraylist内部是没有调用的。内部扩容调用的是ensureCapacityInternal方法。
- 增加容量如果有必要:原则就是判断minCapacity 和当前的capacty大小,如果大则增加。
- minExpand(最小扩展容量)可能是0 ,也可能是DEFAULT_CAPACITY=10。
- 一般ensureCapacity类型的三个方法只是确保最小容量满足,但是真正的增长策略可能不同,比如add一个元素,minCapacity=size+1。
这时候如果需要扩容(minCapacity>size)那么数组只需要增大1,即可,但是这样后面每次都要复制,所以后面真正讲到的grow方法策略不是+1,而是然后大小增加150%。
源码分析:
- minExpand表示最小拓展容量
首先我们看下ArrayList创建的两种创建方法和他的两个常量
ArrayList arr = new ArrayList<>();
ArrayList arr = new ArrayList<>(0);
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
如果使用第一种创建方法那么this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
如果使用第二种创建方法 则this.elementData = EMPTY_ELEMENTDATA;
什么意思?就是说如果你没穿参数,那么Arraylist 设置 默认空数组。如果你传了个0,则认为是你故意设置容量为0的。它用上面两个常量来区分这种行为,区分的目的在于对待这两种不同的空数组, 第一次的容量增长策略不同。
下面我们接着说ensureCapacity方法,就好解释多了,如果elementData!=DEFAULTCAPACITY_EMPTY_ELEMENTDATA 那么minExpand最小扩展容量=0,如果否则minExpand = DEFAULT_CAPACITY=10。
- 委托给ensureExplicitCapacity方法,扩容
然后比较传入参数和minExpand的大小。取其中大者。然后委托给ensureExplicitCapacity方法。
ensureExplicitCapacity
`private void ensureExplicitCapacity(int minCapacity) {
//修改次数 计数器 在iterator迭代出现并发修改快速失败就是利用到该字段
modCount++;
// overflow-conscious code
// 如果minCapacity大于数组长度则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}`
grow方法是真正的增长策略方法:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
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);
}
方法说明:
1. newCapacity先扩展到为原来的1.5倍。
2. 比较newCapacity和minCapacity选择大的。
3. newCapacity如果超过MAX_ARRAY_SIZE(= Integer.MAX_VALUE - 8),则调用hugeCapacity,增大到Integer.MAX_VALUE。
4. 调用Arrays.copyOf调整elementData到新容量。
ensureCapacityInternal
private void ensureCapacityInternal(int minCapacity) {
//如果满足elementData是默认空数组,则比较 minCapacity和DEFAULT_CAPACITY 取大者
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
到此容量增长策略已经分析完成。
扩容方法总结:
- ensureCapacity是public给开发人员调用的api,内部是没有调用的。
- ensureCapacityInternal才是内部判断是否需要扩容的方法。
- 上面两个方法都是调用ensureExplicitCapacity实现。
- ensureCapacityInternal和ensureExplicitCapacity是私有的。
- 这三个方法是判断是否需要扩容,但是不一定真正执行扩容动作,真正的方法是grow。
- 如果我们能够提前预知list容量 直接赋值容量,可以减少后面自动扩容导致的数组复制的开销,调用ensureCapacity直接到一个比较大的容量也是可行的。
List常用方法分析
插入元素
顺序插入add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
确保容量加1,然后直接数组赋值。时间复杂度O(1)。
随机插入
public void add(int index, E element) {
//检查index是否在 0-size范围内
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
因为是随机插入,所以要把插入位置index以及以后的元素向后移动一位。所以速度会慢些。
插入集合addAll
这里不再细讲,大致思路是将Collection转成数组然后插入数组。不过如果是随机插入容量足够 还是复制了两次(第一次向后移动n位,第二次赋值Collection 到指定位置)。
clear清空
很简单 循环将数组设置null
检索操作
主要包括contains,containsAll,indexOf,lastIndexOf。
containsAll循环调用的contaions,contaios调用的indexOf。
indexOf和lastIndexOf差不多,一个从数组顺序查找,一个从数组逆序查找。我们主要看下lastIndexOf。
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
方法也很简单,但是时间复杂度还是非常高的,平均O(n*n)。
get 随机获取元素
速度很快O(1),会先检查index是否合法[0,size)。
remove方法
有四个,根据元素删除,需要遍历数组,找到位置,然后移动数组。根据index删除,直接移动数组。效率都不高。
removeIf是1.8新加的,根据传入lambda 表达式判断是否删除元素,会应用于每个元素,内部通过迭代器遍历元素,判断是否需要该元素。
迭代器
有iterator,listiterator 和Spliterator。
- iterator主要通过内部类实现,存储了一个cursor,默认无参的=0。
- listiterator继承了iterator,实现了可以向前遍历。
- Spliterator(splitable iterator可分割迭代器)接口是Java为了并行遍历数据源中的元素而设计的迭代器,这个可以类比最早Java提供的顺序遍历迭代器Iterator,但一个是顺序遍历,一个是并行遍历。
可以参考:https://segmentfault.com/q/1010000007087438
iterator分析
public Iterator<E> iterator() {
return new Itr();
}
是ArrayLIst一个内部类,截取部分代码:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//每次都会检查modCount是否改变,改变则抛出异常
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
listiterator分析
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
部分代码如上,继承了Itr,添加了逆序遍历功能。
子列表
通过一个内部类实现,部分代码如下:
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
看出来 主要通过一个offset变量来记录开始位置在数组中的起始位置,还是共享父类的数组。
序列化、反序列化
ArrayList实现了Serializable接口,所以它支持序列化,但是我们看到底层存储:
transient Object[] elementData; // non-private to simplify nested class access
又是transient,表示忽略该字段的序列化,其实他是通过writeObject(java.io.ObjectOutputStream s),readObject(java.io.ObjectInputStream s)两个方法重写实现的。注意这两个方法是固定写法。以write为例:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
//记录当前修改次数
int expectedModCount = modCount;
//写入类元信息和非静态成员和非transient成员
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]);
}
//如果序列化过程中发现有修改list结构 则抛出异常
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
上面代码中写了两次size,网上说是兼容老板jdk原因。还提到反序列化老版本会恢复容量 新版本只恢复size大小的容量。
具体参考:
http://mushanshitiancai.github.io/2016/06/17/java/language/JDK%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-ArrayList/#序列化-反序列化
克隆
克隆只是浅拷贝。元素对象还是共享的。
其他总结
用的最多的移动数组方法是Arrays.copyOf和 System.arraycopy。
本文详细介绍了Java中ArrayList的工作原理,包括其内部实现、容量扩展策略、常用方法解析等内容,并深入探讨了迭代器实现及其特性。

584

被折叠的 条评论
为什么被折叠?



