ArrayList概述
arrayList的类图如下:
类结构说明:
- 实现Iterable接口,说明该目标对象允许使用for-each语法
- collection接口,集合类的顶层接口,所有需要集合特性的对象都需要实现该接口。JDK中没有直接实现该接口的类。
- 实现该接口的子接口有List ,Set,Queue。这些接口是根据不同集合的数据结构特点进行衍生的,比如List是有序的队列,有索引,允许有重复值。Set是一个值唯一的集合。Queue是用于保存待处理元素的集合。
- 实现该接口的有AbstractCollection,此类提供Collection接口的骨干实现,用以最大限度减少实现Collection所做的工作(这种做法在集合框架的里被大量用到)
- 实现RandomAccess,Cloneable,serializable,说明该接口可以随意访问,克隆,序列化。
RandomAccess接口说明,实现这个接口说明其支持快速随机访问。
详细说明
==为什么ArrayList继承的AbstractList中已经实现了List方法,为何还要直接实现List接口呢?==
在StackOverflow中找到个答案,大意是实现这样做有利于接口的解释,更清晰。ArrayList实现List接口,继承abstractList接口只是实现该接口的一种方式。
解释链接
内部源码解析
先从ArrayList的构造方法开始入手,然后挑选出常用的方法进行分析源码。
构造方法
ArrayList有三个构造方法
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = 10;
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;
}
public ArrayList(Collection extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//TODO 什么情况下会返回的不是Object[]对象
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList构造方法都是在内部构建一个数组对象。可以自己制定该数组的长度,也可以使用默认初始化长度。重点说明的地方是在public ArrayList(Collection extends E> c)这个构造方法,为什么要有这个方法呢?这个方法提供了Collection之间的互转,比如把set集合转化为list集合
增加
- 增加一个元素 add(E e)
确保容量足够(扩容时需要复制原来的数组);然后在实际数组长度后加上要添加的元素。
public boolean add(E e) {
//保证数组的容量
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//默认为空时,容量初始值为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//这个的作用是什么
modCount++;
// overflow-conscious code,如果已经扩容过一次以后,数组长度增加1.5,此后只有当数组长度不在满足时再继续扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
//数组中扩容1.5倍以后,还是不超过10的,默认初始值为10,超过取扩容值;
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//扩容后的长度,超过int上限值,取传入的最小扩容值
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);
}
- 指定位置添加元素 add(int index, E element)
- 先判断指定位置是否合法
- 保证内部数组的容量
- 原来的数组从index位置开始往后移动一位
- 把要插入的元素放到index位置中
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
ArrayList添加元素都需要用到数组的复制,效率较低。size和elementData.length的区别为:size为ArrayList中元素的个数,elementData.length为实际数组的长度,size<=elementData.length,当size < elementData.length时,超过size后的数组值null。
注解1:无论使用那种类型的数组,数组标识其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象用于保存指向其他对象的引用。elementData[--size] = null;把引用置空,原有的对象没有指向的对象,GC运行时会回收掉。
数组在内存中的存储是否是连续的??--可以创建一个大于heap内存的数组进行试验
==为什么使用来进行扩容 oldCapacity + (oldCapacity >> 1);默认长度为10,在10之内,使用此公式会接近于原来的数组。答:如果已经扩容过一次以后,数组长度增加1.5,此后只有当数组长度不在满足时再继续扩容,因此并不是每次都扩容==
- 在某个位置添加元素
- 验证位置合法性
- 保证数组容量;(增加修改标识)
- index后面的数据往后移动一位
- index位置留给element,容量加1
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//从位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
删除
- 根据位置去删除
- 验证要删除的位置是否合法;
- 把要删除元素位置后面的所有数据向前移动一位;
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
//数组长度减少一位。把后面的数据往前移动,最后一位重复,需要置为空
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//为什么置空以后方便GC工作-注解1
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
- 根据元素去删除
根据ArrayList的容量去遍历,找到需要删除元素的位置,然后根据该元素位置去删除
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;
}
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
}
- 批量删除
批量删除有两个方法,一个是删除传入集合元素,一个是保留传入的集合元素。
public boolean removeAll(Collection> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public boolean retainAll(Collection> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
private boolean batchRemove(Collection> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
//遍历ArrayList,如果符合条件,则重排elementData
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
//??
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
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;
batchRemove里有意思的地方是,当调用c.contains()方法进行判断时就算抛异常也会进行处理,处理的方式是使用try finally来进行。确保最后一定会进入到finally代码块中。
通过判断是否符合条件,来重新排列内部维护的elementData数组对象,保证需要保留的数组元素是从0到w排列。最后把从w到size位置的数组置空。
在循环中删除元素
ArrayList list = new ArrayList(Arrays.asList("a","b","c","d","e","f"));
for (String str: list) {
if("aa".equals(str)){
list.remove("aa");
}
}
使用该种方式进行删除符合条件的元素会抛异常,原因见问题1的解答。
如何正确在循环中删除元素?使用迭代器中的remove方法来进行删除
ArrayList list = new ArrayList(Arrays.asList("a","b","c","d","e","f"));
Iterator iterator = list.iterator();
while (iterator.hasNext()){
if("a".equals(iterator.next())){
iterator.remove();
}
}
以下方式删除也不会报错
ArrayList list = new ArrayList(Arrays.asList("a","b","c","d","e","f"));
int size = list.size();
for(int i = 0;i
但这样删除时错误的,当满足删除条件时,整个list长度变更,导致有些元素会被忽略掉去判断。
迭代器是如何实现删除自身的呢?
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//根据位置删除当前元素,此时ArrayList的size会重置
ArrayList.this.remove(lastRet);
//下一元素游标位置变成当前元素位置
cursor = lastRet;
//-1表示当前没有元素
lastRet = -1;
//modCount在ArrayList.this.remove(lastRet)中已经变更过了,需要重新赋值
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
遍历查询
查询的方法如下:
- 通过索引去查询-速度很快
- 查询对象最后出现的位置,倒叙遍历数组,符合条件返回
- 查询对象第一次出现的位置,正序遍历数组,符合条件返回
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
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;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
- 遍历
有两种方式进行list的遍历,一种是通过迭代器的方式进行遍历,一种是根据索引的方式进行遍历。哪种方式更快呢。
构建一个有1000万元素的list,然后使用两种方式去遍历,看它们的耗时是多少
public class TraverseTest {
public static void main(String[] args) {
List list = new ArrayList<>(10000000);
for(int i=0;i<10000000;i++){
list.add("a"+i);
}
//使用迭代器遍历
long startTime = System.currentTimeMillis();
for (String element:
list) {
String e = element;
}
System.out.println("迭代器遍历耗时:"+(System.currentTimeMillis()-startTime)+"毫秒");
startTime = System.currentTimeMillis();
for (int i=0;i
迭代器模式
ArrayList中使用到一种设计模式-迭代器模式。迭代器模式的语义如下:
迭代器模式又叫游标(cursor)模式,是对象的行为模式。迭代器模式可以顺序地访问一个聚集中的元素,而不用暴露内部的表象(internal representation)-<
>
在java的colleciton框架中每个具体的集合实现类内部都定义了自己的迭代器,这样不同数据结构的集合提供出一个迭代器,用来进行迭代内部数据。让使用者可以不去关心该集合是List,还是set(Map有不同的迭代方式)。
- 迭代器(Iterator):定义遍历元素需要的方式,一般有判断是否存在下一个元素的hasNext(),取得下个元素的方法next(),移出当前对象的方法remove().
- 具体迭代器(concreteIterator):迭代器的具体实现,在集合框架中,一般以私有内部类的形式来实现。
- 容器角色(Aggregate):一般是一个接口,提供一个iterator()方法,如Collection接口,List接口,Set接口等。
- 具体容器角色(concreteAggregate):容器角色的具体实现,如List接口的有序实现类ArrayList。
具体迭代器中持有一个具体容器角色,该角色是具体需要遍历的对象。看下ArrayList中的迭代器实现
private class Itr implements Iterator {
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() {
//用于检验在遍历过程中该集合的数据结构是否被改变
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
容错方式-fail-fast
如果一个算法开始之后,它的运算环境发生变化,使得算法无法进行必需的调整时,这个算法就应当立即发出故障信号。这就是Fail Fast的含义。
从上面的迭代器源码上可看出,在使用迭代器遍历ArrayList的时候,不能变更该集合的结构(删除,或者添加),否则在checkForComodification()时会抛出异常。
由于ArrayList是线程不安全的,当一个线程在遍历集合时,另一个线程进行改变集合的结构时(删除,或者添加),会抛出ConcurrentModificationException异常,java文档中对fail-fast的表述是,这对并发修改不做硬性保证,不建议针对这个异常来进行保证并发,它仅仅只是用来检测bug。如果需要线程安全的list,可以使用CopyOnWriteArrayList
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
待完善的点
java8 新增的方法解析
线程安全的有序List
总结
- ArrayList是存在下标,随机访问效率高
- ArrayList随机插入和删除效率比较低,需要用到数组复制
- 使用for循环中,根据下标访问效率比使用foreach循环访问高(迭代器访问)
- ArrayList的增删都不是线程安全的,在foreach循环中删除或者增加元素,会引起fail-fast
问题:
- 1.为什么在foreach遍历对象时不能修改对象的结构
使用foreach循环,在编译器编译成class文件后,会对代码进行优化,会变成迭代器循环
ArrayList list = new ArrayList(Arrays.asList("a","b","c","d","e","f"));
for (String str:
list) {
if("aa".equals(str)){
System.out.println(str);
}
}
编译成class文件以后
Iterator set = list.iterator();
while(set.hasNext()) {
String strings = (String)set.next();
if("aa".equals(strings)) {
System.out.println(strings);
}
}
在迭代器里面每次调用next去获取下一个元素时,会去验证iterator中的expectedModCount和ArrayList中modCount是否符合当前相等,不相等抛出会抛出ConcurrentModificationException异常。Arraylist中的modCount在修改集合的结构时,会增加。异常如果是expectedModCount!=modCount,则说明有线程修改了ArrayList的结构。
- 2.为什么使用Transient修饰elementData?
因为elementData是动态扩容的,如果全部都序列化,数组中会多出很多指向为null的数据。比如元素数量为16的时候,16*1.5=24,多出8个多余的元素。当arrayList中元素较多时,浪费的空间还是蛮可观的。那么如何保证arrayList序列化时,elementData中的数据能序列成功?ArrayList重写了writeObject(java.io.ObjectOutputStream s)和readObject(java.io.ObjectInputStream s)用于elementData的序列化和反序列化
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而不是elementData.length来进行的。保证了元素的有效性。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i
-
- 为什么JDK中的集合类,比如List一族或者Set一族,都是实现了Iterable接口,但并不直接实现Iterator接口?
因为Iterator接口的核心方法next()或者hasNext() 是依赖于迭代器的当前迭代位置的。
如果Collection直接实现Iterator接口,势必导致集合对象中包含当前迭代位置的数据(指针)。
当集合在不同方法间被传递时,由于当前迭代位置不可预置,那么next()方法的结果会变成不可预知。
除非再为Iterator接口添加一个reset()方法,用来重置当前迭代位置。
但即时这样,Collection也只能同时存在一个当前迭代位置。
而Iterable则不然,每次调用都会返回一个从头开始计数的迭代器。
多个迭代器是互不干扰的
- 为什么JDK中的集合类,比如List一族或者Set一族,都是实现了Iterable接口,但并不直接实现Iterator接口?
参考资料
- 集合概述
- 学习集合有用的博客