一、ArrayList的整体结构
1、重要属性
//Default initial capacity 初始化容量
private static final int DEFAULT_CAPACITY = 10;
//空实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//初始化时的实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//当前List容器
transient Object[] elementData;
//The size of the ArrayList (the number of elements it contains).
//容器元素的数量,也是元素的下标
private int size;
//结构被修改的次数(AbstractList 中的属性)
protected transient int modCount = 0;
transient:在JAVA中一个对象只要实现了Serilizable接口,这个对象就可以被序列化,被序列化的对象可以在网络中传输,也可以存在磁盘中,然后通过反序列化可以获取原数据。但是对于一些敏感的数据,如银行卡密码、用户登录名、密码等数据我们不希望在网络中传播,也不希望存储在磁盘中。对于这样的数据可以使用transient修饰。简单说当在需要序列化的属性前添加关键字transient后,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
2、数据结构
Objecte类型的数组
3、特点
- 有序
- 允许null元素
- 允许重复
- size、isEmpty、get、set、add 等方法时间复杂度都是 O (1);
- 非迭代器遍历修改时容器大小时会抛出ConcurrentModificationException
- 非线程安全,可以List list = Collections.synchronizedList(new ArrayList(...));转线程安全
二、源码
1、构造函数
//1、指定容量大小的构造器
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);
}
}
//2、无参构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//3、传入一个集合的构造器
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 对于第一个构造函数当initialCapacity=0时,数组被初始化位空数组,数组的初始大小是在第一次add的时候赋值10
- 对于第二个构造函数,数组被初始化为默认类型的空数组,数组的初始大小是在第一次add的时候赋值10
- c.toArray might (incorrectly) not return Object[] (see 6260652) 这是JDK中的一个BUG编号; c.toArray();得到的数组不一定就是Object类型的数组 ,所以在这里做了一个if()判断,然后通过Arrays.copyOf(elementData, size, Object[].class);将其转化为Object类型的数组。例如以下情况
List<String> list = Arrays.asList("abc");可以知道返回的实际类型是java.util.Arrays$ArrayList,而不是ArrayList。我们调用Object[] objArray = list.toArray();返回是String[]数组,所以我们不能将Object对象,放到objArray数组中。
2、添加元素与扩容
添加元素:
public boolean add(E e) {
//保证数组的容量可以添加当前元素
ensureCapacityInternal(size + 1); // Increments modCount!!
//在当前size上放入元素,并将size+1(在ArrayList中并没有成员变量index,他是直接使用size做下标)
elementData[size++] = e;
return true;
}
扩容:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果初始化的数组是默认的空数组(无参构造函数初始化)就返回默认容量10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//数组被修改记录+1
modCount++;
// 如果数组的添加一个元素的实际容量 > 当前数组的容量,就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//数组的扩容
private void grow(int minCapacity) {
// 当前数组的容量
int oldCapacity = elementData.length;
//新的容量=老的容量+老容量的一半(右位移一位就是除以2向下取整)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新的容量 < 我们期望的容量,就采用期望的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//新的容量 > MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) ,就采用Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
数组的copy:数组扩容的本质就是大数组与原来小的数组进行copy
//数组copy,该方法是JDK提供给我的本地方法,
//他是浅copy,非线程安全,底层是对内存的直接复制,所以相比于for循环,效率高出很多
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
Object src : 原数组
int srcPos : 从原数组的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos,int length);
3、删除元素
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++)
//找到第一个非null元素的下标
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
//数组被修改记录+1
modCount++;
//要移动元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
//将移除的位置补齐(在这里采用的是数组copy的方式)
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//移除一个元素后将释放占用的空间
elementData[--size] = null; // clear to let GC do its work
}
删除数据的核心点:当移除一个元素后,为了保证数组的连续性,并且释放调没用的空间,他需要把空出来的位置填补上;简单数说就是被删除元素后面的元素依次向前移动一位。
ArrayList采用的方式:被删除元素前面不动,把其之后的元素copy到目标数组中,copy的起点是被删除元素的下标,copy长度是其之后元素个数
如图数组我们要删除C元素:System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
- index=2
- srcPos=index+1=2+1=3;
- destPos=index
- numMoved=6-2-1=3
- length=numMoved=3
- src=elementData
- dest=elementData
4、迭代器
ArrayList 有自己的迭代器
private class Itr implements Iterator<E>
核心成员:
int cursor; // 下一个元素的索引
int lastRet = -1; // 最后一个元素的索引,如果没有返回-1
int expectedModCount = modCount;//数组被修改记录(数组版本)
迭代的核心方法:
//判断是否存在下一个元素
public boolean hasNext() {
//下一个元素的索引如果与size(元素个数)相等,表示没有元素可迭代
return cursor != size;
}
//取出元素
@SuppressWarnings("unchecked")
public E next() {
//检查数组是否被修改 ,版本不一致 ConcurrentModificationException
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还原;当前元素已经删除了,并且被后续元素补齐了,所以cursor不变
cursor = lastRet;
//删除元素设为-1,主要目的就是防止重复删除
lastRet = -1;
//将检查版本的属性版本号给补齐,避免下次检察的不一致
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
看了源码后我想大家明白了为什么在循环中删除元素要使用迭代器,而不是普通的循环了
迭代其删除元素:做了这两件事
- //移除后将cursor还原;当前元素已经删除了,并且被后续元素补齐了,所以cursor不变
- cursor = lastRet;
- //将检查版本的属性版本号给补齐,避免下次检察的不一致
- expectedModCount = modCount;
普通循环中删除元素:直接操作的是数组,会存在遍历丢失
三、总结
1、时间复杂度:
- add(E e):添加元素到末尾,时间复杂度为O(1)
- add(int index, E element):添加元素到指定位置,时间复杂度为O(n)
- get(int index):获取指定索引位置的元素,时间复杂度为O(1)
- remove(int index):删除指定索引位置的元素,时间复杂度为O(n)
- remove(Object o):删除指定元素的元素,时间复杂度为O(n)
2、线程安全:
- ArrayList 作为共享变量时,会存在线程安全问题,如果ArrayList 是方法内的局部变量,是没有线程安全的问题的
- ArrayList 存在线程安全问题的本质是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,并且这些变量的类型并非是volatile的,也不是原子操作。
- 类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 锁的粒度是整个方法,实现了线程安全,但是性能降低,具体实现源码:
public void add(int index, E element) { synchronized (mutex) { list.add(index, element); } }