目录
4.2.1移除指定位置的元素 remove(int index)
1.简介
它是一种基于动态数组的集合类,继承了AbstractList,实现了RandomAccess,Cloneable,Serializable,List接口,是可快速随机访问,可可隆,可序列化的。size , isEmpty , get , set , iterator和listIterator操作在常数时间内运行。add运算以摊销的固定时间运行,即添加n个元素需要O(n)时间。 所有其他操作均以线性时间运行(大致而言)。 与LinkedList实现相比,常数因子较低。请注意,此实现未同步。 如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素或显式调整后备数组大小的任何操作;仅设置元素的值不是结构修改。)这通常是通过对某些自然封装了对象的对象进行同步来实现的。如果不存在这样的对象,则应使用Collections.synchronizedList方法“包装”列表。 最好在创建时完成此操作,以防止意外的不同步访问列表。
另外此类的iterator和listIterator方法返回的iterator是快速失败的:如果在创建迭代器后的任何时候以任何方式对列表进行结构修改,则除了通过迭代器自己的remove或add方法之外,迭代器都会抛出ConcurrentModificationException 。 因此,面对并发修改,迭代器会快速干净地失败,而不会在未来的不确定时间冒着任意,不确定的行为的风险。
应用程序可以使用ensureCapacity操作在添加大量元素之前增加ArrayList实例的容量。 这可以减少增量重新分配的数量。
2.属性
//默认容量
private static final int DEFAULT_CAPACITY = 10;
//用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认大小的空实例数组
//区别:若是默认的,在第一次调用add方法才会扩容至DEFAULT_CAPACITY(10)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储ArrayList的元素的数组缓冲区,非私有以简化嵌套类访问
transient Object[] elementData;
//ArrayList的长度,即包含元素的个数
private int size;
EMPTY_ELEMENTDATA用在有参构造函数当初始容量为0时共享赋值用,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 用在无参构造函数赋值用
两者都是用来减少空数组的创建,所有空ArrayList都共享空数组。两者的区别主要是用来起区分用,针对有参无参的构造在扩容时做区分走不通的扩容逻辑,优化性能。
在无参构造函数创建ArrayList时其实创建的是一个容量为0的数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA 标识),只有在第一次新增元素时才会被扩容为10
3.构造方法
ArrayList提供了三个构造方案:
//构建一个指定容量的空list
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; //如果指定容量为0,则elementData赋值为成员变量中的空实例数组(长度为0)
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//无参构造方法,将空的DEFAULTCAPACITY_EMPTY_ELEMENTDATA引用传给elementData,用来标识此次创建时默认的创建方式,用来后续add中调用grow进行首次数组的扩充,扩充到容量为10的数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//根据其他集合创建list
public ArrayList(Collection<? extends E> c) {
// 此处调用了toArray方法,但因为该方法可能不会返回Object[]类型,因此为了保证类型有以下的判断
Object[] a = c.toArray();
if ((size = a.length) != 0) {
//如果是object类型,直接赋值
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
//如果不是object类型,则将a中元素copy到elementData中,类型为object
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
//如果长度为0,则赋值为实例空数组(长度为0)
elementData = EMPTY_ELEMENTDATA;
}
}
记一下Object中toArray方法
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
4.操作
4.1增加元素
//增加一个元素到list中
public boolean add(E e) {
//对list的修改次数(每次add和remove都会增加,是AbstractList抽象类的成员变量)
//便于迭代器快速失败
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
//当导入的size等于数组长度时,需要扩容到size+1
if (s == elementData.length)
elementData = grow();
//导入一个元素未超过数组容量,直接加入到数组当中,下标为s,长度+1,
elementData[s] = e;
size = s + 1;
}
//无参构造,返回调用grow(size+1);
private Object[] grow() {
return grow(size + 1);
}
//增加容量以确保其至少可以容纳最小容量参数指定的元素数
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
//判断数组是否为空,如果不为空,通过工具类ArraysSupport的newLength方法为ArrayList计算
//新的容量大小.然后通过Array类中copyOf方法将旧的数组复制到新的数组中(数组长度为
//newLength方法计算),底层的创建数组和复制数组都是native方法实现。
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity,
oldCapacity >> 1;
return elementData = Arrays.copyOf(elementData, newCapacity);
//如果数组为空,或者是初次创建的数组(通过DEFAULTCAPACITY_EMPTY_ELEMENTDATA标识),则新建一默认大小的数组
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
//将指定的元素插入此列表中的指定位置。 将当前在该位置的元素(如果有)和任何后续元素右移(将其索引加一)
public void add(int index, E element) {
//判断index是否合理
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
//检查数组大小是否能容纳新加入一个元素
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
//index后的元素往后移动一格
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
/*ArrayCopy参数含义:
第一个参数:原数组
第二个参数:从原数组的此下标开始
第三个参数:目标数组
第四个参数:从目标数组的此下标开始
第五个参数:copy的长度
*/
4.2移除操作
4.2.1移除指定位置的元素 remove(int index)
//移除指定位置的元素
public E remove(int index) {
//判断index小于0或者大于size的情况,抛出异常()
Objects.checkIndex(index, size);
//将引用赋值给es
final Object[] es = elementData;
//获取到index位置的元素,便于后续返回该删除的元素,然后调用fastRemove快速删除index位置的元素
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
//快速移除指定位置的元素
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
//checkIndex最终调用这个函数,该处第三个参数为null
int checkIndex(int index, int length,
BiFunction<String, List<Integer>, X> oobef) {
if (index < 0 || index >= length)
throw outOfBoundsCheckIndex(oobef, index, length);
return index;
}
4.2.2移除指定元素 remove(Object o)
//移除指定元素,可为null
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {//所有移除的操作都是下标从小到大遍历,并移除第一个与o相等的元素
//当所需移除为空时
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {//当元素不为空时
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}//找到元素下标,并调用fastRemove方法移除该元素
fastRemove(es, i);
return true;
}
4.3获取操作
//底层是数组,根据元素下标获取对应元素,当然首先会判断该下标是否符合规范
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
5.克隆机制
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
//调用copyOf方法复制元素
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
//这个不可能发生,因为实现了Cloneable
throw new InternalError(e);
}
}
6.序列化机制
上面我们说到ArrayList实现了序列化接口Serializable,说明是可以被序列化的。但是同时我们在ArrayList源码中的成员变量中发现,存放实际元素的数组elementData是被transient修饰的,说明它不被序列化。但是实际测试结果证明,ArrayList能够被序列化,同时elementData也能够经过反序列化后获取到。为什么明明用transient修饰还是能够被序列化?加上transient又是为了什么?其实默认的序列化,调用的是ObjectOutputStream 的 defaultWriteObject( )以及 ObjectInputStream 的 defaultReadObject( )。但是ArrayList类中定义了writeObject() 和 readObject ()。序列化过程中,虚拟机允许类自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程。(节选自小咸白鱼)
@java.io.Serial
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
int expectedModCount = modCount;
s.defaultWriteObject();
// 写入实际元素个数
s.writeInt(size);
// 循环写入元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
@java.io.Serial
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 读出所有数据
s.defaultReadObject();
// 读入容量
s.readInt(); // ignored
if (size > 0) {
//像clone()一样,根据大小而不是容量分配数组
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
Object[] elements = new Object[size];
// 按正确的顺序读入所有元素
for (int i = 0; i < size; i++) {
elements[i] = s.readObject();
}
elementData = elements;
} else if (size == 0) {
elementData = EMPTY_ELEMENTDATA;
} else {
throw new java.io.InvalidObjectException("Invalid size: " + size);
}
}
通过查看源码,很明显ArrayList不会走序列化的默认方法,而是走自定义的方法。因为elementData经过扩容后,有可能后面有很大的空间都没有存放元素,若按照默认的序列化方式,则这些空间也需要同时进行序列化保存,浪费空间。
所以elementData修饰为transient,不能进行序列化,然后通过自定义的序列化方法,类似于trimToSize()方法的效果,序列化的数组长度等于实际元素个数。