一家之言 姑妄言之 絮絮叨叨 不足为训
ArrayList类注释翻译:
ArrayList是一个实现了List接口的可改变大小的数组结构。它实现了关于List集合所有的操作,并且允许存有所有的类型元素,包括null类型。
ArrayList除了实现了List接口之外,还提供了一系列用来操作对存储元素数组大小的方法(ArrayList大致相当于Vector,只不过它是不同步的)。这个类里面的size
、isEmpty
、get
、set
、iterator
,以及listIterator
操作可以在常数时间内完成,并且add操作会在分段常数时间内完成。也就是说,添加n个元素需要O(n)的时间。那么粗略地说,所有的其他操作都可以在线性时间内运行。
相较于LinkedList的实现,ArrayList的常数因子还是较低的。
在这里,每一个ArrayList实例都有其自身的容量,这里所谓的“容量”则是用于存储于列表中的元素数组的大小。而这个“容量”始终大于等于列表的长度,即list.size()
。
当一个元素添加到ArrayList中时,其容量还会自动扩容。那么事实上这个增长策略的细节是没有指定的,但是当添加一个元素的时候其开销是均摊常数时间。在使用ensureCapacity()
操作添加大量元素之前,我们的应用程序可以增加ArrayList实例的容量。这样的操作可能会减少增量再分配的次数。
当然,这个实现不是同步的。如果有多个线程同时访问一个ArrayList实例,并且至少有一个线程从结构上修改了列表,那么这组操作必须在外部做好同步(任何对ArrayList的添加或删除操作,添加一个或多个元素或删除一个或多个元素,或是显式的修改ArrayList中备份数组的大小等这类操作都是一种结构性修改。当然,仅仅设置元素的值并不属于结构修改)。这通常是通过对一些自然封装列表的对象进行同步来实现的。如果不存在这一类对象,那么列表就可以使用Collections.synchronizedList()
方法来对List同步。当然这种操作最好是在创建的时候完成,以防止突发性的非同步访问List。例如:
List list = Collections.synchronizedList(new ArrayList(...));
ArrayList这个类的迭代器和listIterator方法返回的迭代器具有fail-fast的机制:即,当我们在创建迭代器之后,一旦对List进行了结构上的修改(当然除了通过iterator自己的删除或添加方法),iterator将会抛出ConcurrentModificationException异常信息。
因此,在面对并发修改时,iterator会快捷的地失效,而不是在未来某个不确定的时间冒着突然地、不确定行为的风险进行迟缓的失效。不过我们需要注意的是,fail-fast机制是不能被完全保证的,因为一般来说,我们不能在存在非同步并发修改的情况下做出任何严格的机制保证。
注意,我们不能保证迭代器的快速故障行为,因为通常来说,在存在非同步并发修改的情况下,谁都不可能做出任何严格的绝对保证。但是fail-fast将会在最大努力的基础上抛出ConcurrentModificationException异常。因此,如果我们想依赖于这个fail-fast机制来编写程序将会是一个错误的选择。所以,我们的iterator的fail-fast机制应该只用于检测bug,而不是依此来进行复杂业务处理。
最后,ArrayList是Java集合框架的成员。
ArrayList类信息:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
我们可以清楚的看到,ArrayList一个是继承自AbstractList并实现了List
接口、RandomAccess
接口、Cloneable
接口、java.io.Serializable
接口的类。
ArrayList静态变量信息:
/* 序列化版本编号 */
private static final long serialVersionUID = 8683452581122892189L;
/* 默认初始化容器大小 */
private static final int DEFAULT_CAPACITY = 10;
/* 用于一系列空数组实例的共享数组实例 */
private static final Object[] EMPTY_ELEMENTDATA = {};
/*
* 用于默认大小的空实例的共享空数组实例。我们将其与EMPTY_ELEMENTDATA区分开来,
* 以明确当第一个元素被添加时什么时候可以进行数组的扩容。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 需要分配的数组的最大容量。
* 一些虚拟机会在数组中保留一些标题词。
* 如果我们试图分配较大的数组容量,可能会导致OutOfMemoryError错误:
* 请求的数组大小超过虚拟机限制。
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
我们来看ArrayList的静态变量信息。
首先,因为ArrayList实现了java.io.Serializable
接口,所以会生成一个序列化版本编号,这个属性个人认为无需赘言。自动忽略即可。
其次,DEFAULT_CAPACITY
也明确了我们的ArrayList的默认初始化容器大小为10。也就是说,如果我们使用无参构造器创建ArrayList时,当添加第一个元素之后,我们的ArrayList将会将其数组容量扩展为默认的大小10。
再次,EMPTY_ELEMENTDATA
声明了一个空的、类型为Object的数组。
从次,DEFAULTCAPACITY_EMPTY_ELEMENTDATA
也声明了一个空的、类型为Object的数组。不过我们可以从该属性的源码注释中可以看到,其区别于EMPTY_ELEMENTDATA
空数组。从其他资料上来看,这样的做法在JDK8中,EMPTY_ELEMENTDATA
在一定程度上减少了空数组的存在,降低了内存的消耗,在一定程度上是对性能的优化。
最后,MAX_ARRAY_SIZE
声明了我们的ArrayList所允许的最大容量,也就是ArrayList所能包含的最大元素个数。其最大容量为Integer.MAX_VALUE - 8
,也就是2147483647-8 = 2147483639
。这里面的减8操作从其注释里可以明确的是,在一些虚拟机里面,因为虚拟机的类型不同,如果声明了最大的Integer数值,可能会造成OutOfMemoryError错误。所以这个属性可以理解为对ArrayList容量的一个合理范围。
不过需要注意的是,在ArrayList中的hugeCapacity(int minCapacity)
方法中也可以清晰的看到,如果最小容量(minCapacity)大于了我们的MAX_ARRAY_SIZE
,我们还是会给ArrayList的容量分配最大的Integer数值(Integer.MAX_VALUE-2147483647)。不过我个人相信,一般不会发生这种情况,这种极端的情况完全是可以忽略不计的。
ArrayList成员变量信息:
/**
* 存储ArrayList元素的数组缓冲区。ArrayList的容量就是这个数组缓冲区的长度。
* 任何elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组,都会在第
* 一个元素添加后将其容量扩展成为DEFAULT_CAPACITY,也就是10。
*/
transient Object[] elementData; // 非私有以简化嵌套类访问
/* ArrayList的大小(也就是ArrayList包含的元素个数)。 */
private int size;
我们来看ArrayList的成员变量信息。
首先,elementData
依旧是一个Object类型的空数组。只不过这个数组用transient
,也就说明了该属性是不参与ArrayList的序列化操作的,这样做的目的可能是为了减少磁盘的占用率,也可能是为了安全。从其他一些资料来看,减少磁盘占用率关键词出现的次数要往往大于安全这个关键词的字数。
其次,size
这个属性就不需要多说了,其代表的就是我们ArrayList内部存储的元素个数。
ArrayList构造函数信息:
/** 构造一个初始容量为10的空列表 */
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造具有指定初始容量的空列表。
* @param initialCapacity 构造ArrayList时,初始化的容量
* @throws IllegalArgumentException 如果指定的初始容量为负数,则抛出IllegalArgumentException异常。
*/
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);
}
}
/**
* 构造一个包含指定集合的元素的列表,按集合的iterator返回元素的顺序排列。
* @param c 要将其元素放入此列表的集合
* @throws NullPointerException 如果指定的集合为空,则抛出NullPointerException异常
*/
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;
}
}
首先,是我们的无参构造器public ArrayList()
,这个无参构造器用其注释里面的话来说,它会构造一个初始容量为10的空列表。但是我们从源码方面观察到,当我们调用这个无参构造器构造ArrayList时,也仅仅是将DEFAULTCAPACITY_EMPTY_ELEMENTDATA
空数组赋给了elementData
属性,并没有注释所说的初始化容量为10的现象。
其实elementData的源码注释已经明确说明了此种情况:
Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
也就是说任何elementData
为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的空数组,都会在第一个元素添加后将其容量扩展成为DEFAULT_CAPACITY,也就是当我们在调用了add(E e)
方法之后,我们的ArrayList的数组长度才会变为10。
其次,就是我们的有参构造器public ArrayList(int initialCapacity)
,这个构造方法指定了创建ArrayList时,ArrayList所能拥有的初始化容量。也就是说我们可以创建不大于MAX_ARRAY_SIZE
的任意大小的ArrayList。这个方法里面会对initialCapacity
也就是初始容量进行判断:
如果大于零,那么就会创建出一个类型为Object,容量为initialCapacity
的数组并赋予elementData
。如果等于零则会将空数组EMPTY_ELEMENTDATA
赋予elementData
(注意:这里的空数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
)。除了以上两种情况,也就是说initialCapacity
并不大于等于零,而是负数,这个时候我们的程序就会抛出IllegalArgumentException
异常。
最后,我们的最后一个有参构造器public ArrayList(Collection<? extends E> c)
,这个构造方法我们可以明显的看到它在往构造函数的参数里面传递集合类型的数据。而这个集合的类型必须符合我们当前ArrayList规定泛型的子类或其本类。
那么,这个构造方法是先将传入的集合转为数组并赋予elementData
。然后将这个elementData
的长度赋予size
属性,并判断其是否不等于零。这个判断其实就是在判断我们传进来的这个集合是否为空。那么如果为空,就会创建一个空数组,也就是我们看到else语句中的EMPTY_ELEMENTDATA
。重点就在于如果不为空,还需要判断一下该数组的类型是否为Object类型,如果不是的话,则通过Arrays.copyOf(elementData, size, Object[].class)
函数对已有的elementData
数组进行一次复制重组,约定其为Object类型的数组。需要注意的是:这里进行的一次elementData
类型判断是为了避免JDK本身的一个bug,这个bug已经在JDK1.9里进行了修复,如果想要了解具体的情况,可以根据其提供的注释来查看具体的bug信息:
c.toArray might (incorrectly) not return Object[] (see 6260652)
ArrayList的功能性方法解析:
/**
* 将ArrayList实例的容量调整为列表的当前大小。
* 应用程序可以使用此操作最小化ArrayList实例的存储。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
这个方法的作用从注释上面就可以看出它其实是用来缩减数组大小开支的。我们仔细来看这里面的操作。首先该方法除了会对修改次数modCount
进行+1操作外,接下来就是对当前ArrayList的元素个数和当前ArrayList的elementData
数组长度进行对比。如果个数小于了数组长度,那么就开始进行数组长度的缩减(这个不难理解,因为正常情况下,我们的元素个数是不可能超过,也就是大于数组整体长度的,否则就下标越界,出问题了!)。
那么接下来的操作就是对数组元素的个数进行与零的比较,如果元素个数为空,也就是说根本就没有元素,显而易见我们的elementData
就是一个空数组EMPTY_ELEMENTDATA
。否则,我们就会调用Arrays.copyOf(elementData, size)
方法进行数组的复制。重点就是这个方法的第二个参数表示了新数组的长度,而我们传入的就是我们当前ArrayList内部元素的个数。显式的告诉了我们ArrayList内部元素的个数就是底层数组的长度。
/**
* 如果必要,增加这个ArrayList实例的容量,
* 以确保它至少可以容纳由minCapacity参数指定的元素数量。
* @param minCapacity 所需的最小容量
*/
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);
}
}
这个方法的作用其实从字面意思来看就是单纯的保障我们ArrayList的最小容量。我们从代码入手,首先它是需要得到一个minExpand
值,也可以叫做最小扩展值,它是拿我们当前的数组elementData
与ArrayList的默认空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
进行了比对。如果两者不相同,那么最小扩展值就会取0,否则就是ArrayList的默认容量DEFAULT_CAPACITY
,即10。
接下来我们才会根据我们传入的最小容量minCapacity
与上述的minExpand
进行比对,如果我们当前指定数组的最小容量大于了我们得到的最小扩展值,我们就必须对其进行扩容。
这个我们不难理解,当我们需要添加一个元素时,为避免我们的元素在数组内部超出范围(下标越界),必须适当地对其进行合理的容量判断。当我们所需要的最小空间都已经比实际空间大了,那么我们这个时候就需要进行必要的扩容了。
不过该方法在这里仅做简单的阐述,并不具体详细讲解,因为根据源码来看,该方法的调用者是由com.sun.corba.se.impl.orbutil.DenseIntMapImpl
这个类里面的ArrayList调用,超出了本篇文章讲解范畴,说白了就是逻辑无法对接上,我想不过来,所以在这里就点到即止了。
/**
* 确保内部容量
* @param minCapacity 最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 确保明确的内部容量
* @param minCapacity 最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0) grow(minCapacity);
}
/**
* 计算数组容量
* @param elementData 需要计算的数组
* @param minCapacity 最小容量
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
在这里我们将会一同讲解三个方法,因为上述的三个方法息息相关。
我们首先说的就是第一代码段ensureCapacityInternal
这个方法。通过观察,该方法被add(E e)
、add(int index, E element)
、addAll(Collection<? extends E> c)
等一系列添加行为的方法调用。而这个方法的本质也是在保证数组内部容量是否合理。其实不难理解,当我们向ArrayList,也就是数组添加元素时,我们确实是需要知道我们的数组容器是否还能容纳这些元素,抑或是我们的这些元素,是否能按照需求放入指定的容器位置里面。这就需要我们建立一个函数来观测我们当前数组的健康状态,一旦发现无法容纳当前诸多元素,那么就会理解采取扩容的机制来对容器长度就行扩大。这样就有效的避免了IndexOutOfBoundsException
异常的出现。
从上述三段代码段的代码中可以看到,ensureCapacityInternal
会调用ensureExplicitCapacity
方法,而我们的这个ensureExplicitCapacity
方法还会调用calculateCapacity
方法。
所以,现在我们要讲解的则是第三代码段calculateCapacity
。这个方法接收两个参数,一个是需要计算的数组,一个是最小容量。这里的Object[] elementData
其实就是我们ArrayList内部的elementData
数组。我们看,它会将这个elementData
与DEFAULTCAPACITY_EMPTY_ELEMENTDATA
进行比对,询问是否是我们的空数组。如果不是的话,直接返回我们传入的minCapacity
,因为这个minCapacity
存在的前提是我们已经计算好且明确了的最小容量。相反,如果比对正确的话,就返回DEFAULT_CAPACITY
或minCapacity
中的最大值,简单来说要么就是默认的10,要么就是给定的比10还要大的最小容量。因为我们考虑两种情况,一种是向空数组内存入小于10这个容量的多个元素,这个时候我们所需保证的容量只要不超过10即可;另一种则是存入另一组集合,而这个集合可能是11,12,20等超出默认容量10的多个元素,所以这个时候我们必须保证当前数组的容量必须是我们传进的最小容量。
至此,我们开始我们的第二代码段ensureExplicitCapacity
。
这个方法承前启下,它在自增修改次数后,会对传入的minCapacity
与当前的数组elementData
的长度进行比对。这是判断是否扩容的关键一步,它在判断我们需要的最小容量是否超出了当前数组本身的容量,如果所需的容量超出了当前数组本身的容量,那毋庸置疑,我们的数组是需要扩容的,即,调用grow
方法进行扩容。
/**
* 增加容量,以确保它至少可以容纳由最小容量参数指定的元素数量。
* @param minCapacity 所需的最小容量
*/
private void grow(int minCapacity) {
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通常接近于size,所以这是一个胜利
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 返回最大容量
* @param minCapacity 所需的最小容量
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
我们接下来讲解的是另一个核心方法——grow
方法。这个方法是ArrayList里面一个极为重要的方法。因为它涉及了如何对ArrayList进行扩容。这里需要说明的是,所谓的ArrayList扩容无非就是数组的扩容,其本质就是数组的复制。
我们仔细来看,当我们的grow
拿到了最小容量后(这个minCapacity
是由ensureExplicitCapacity
方法传入进来的,也就是计算好之后当前数组所需的最小容量),并不会直接参与进去。而是还要获取当前数组的长度,并将此长度赋予oldCapacity
,然后通过这个oldCapacity
来计算新的数组容量newCapacity
。
这个newCapacity
的计算方法是利用oldCapacity
加上oldCapacity
右移一位后的得数之和。说白了,就是将现有的oldCapacity
除以2后再加上其本身,也就是所谓的扩展原先数组的1.5倍。
那么接下来我们继续往下走。当两个新旧容量计算出来的时候,还要进行一次新容量newCapacity
与最小容量minCapacity
的对比,如果计算出来的新容量newCapacity
要小于最小容量minCapacity
,那么我们就把这个minCapacity
赋予newCapacity
,也就是最终我们的数组长度是根据这个newCapacity
来指定的。
上述的这种做法我们不难理解,譬如我们在对ArrayList进行添加操作时,如果添加的是一个大型的集合呢?可能本身我们的数组长度为10,计算后扩容的结果为15,但是传进来集合长度就已经为30了,也就是所谓的minCapacity
为30,这时候难道我们还需要用计算后的长度吗?显而是不可能的,我们必须把这个minCapacity
作为我们新扩容数组的长度。
好的,我们接着说下一句。当上述的判断完成之后,其实还需要再判断一次,也就是代码中写的newCapacity
要和MAX_ARRAY_SIZE
进行一次比对。如果我们的newCapacity
比ArrayList所规定的最大容量MAX_ARRAY_SIZE
还要大,那我们只能调用hugeCapacity
方法进行最大容量的分配了。
注意:这里是将minCapacity
传入了hugeCapacity
方法,而不是将newCapacity
传入。这个行为下面会做阐述。
好,到这里我们就已经计算出了当前数组所需要的最合适的容量,也就是我们的最小容量minCapacity
,接下来只需要利用Arrays.copyOf
这一步数组复制就可以了。
至此,ArrayList的动态扩容我们就完成了!
当然,这里我们简单的描述一下hugeCapacity
这个方法。这个方法其实就是为了分配数组所能承担的最大容量。这个方法的调用就是我们上面所说的当最小容量minCapacity
大于了ArrayList所规定的最大容量MAX_ARRAY_SIZE
,我们就需要对其进行最大容量的分配。首先还是对这个传入的容量进行判断,如果小于0,那么就直接抛出OutOfMemoryError
异常,否则我们就对其分配所谓的最大容量。然而这里其实还需要进行一次判断,即,传入的最小容量minCapacity
是否小于我们的最大容量MAX_ARRAY_SIZE
。这个判断不难理解,因为之前计算好的newCapacity
仅仅是经过1.5倍计算过后的容量值,它可能超出了Integer.MAX_VALUE
(2147483647),也可能实际需要的还未到达Integer.MAX_VALUE
(2147483647)。所以前者解释了我们在利用hugeCapacity
进行扩容的时候接受的参数是最小容量minCapacity
而不是newCapacity
,因为如果用到了超出最大容量的数值,就一定会造成错误。那么后者,也就是解释了为什么还需要一层与MAX_ARRAY_SIZE
对比的原因,即,如果我们数组所需的最合理的最小容量minCapacity
并不超过ArrayList里面定义的MAX_ARRAY_SIZE
(2147483639),那么我们为什么还需要分配Integer.MAX_VALUE
呢?仅仅分配当前的MAX_ARRAY_SIZE
就好了。除非,就是发生了最坏的结果,我们需要的容量超出了MAX_ARRAY_SIZE
,因此只能分配数组所需要的最大空间Integer.MAX_VALUE
了。
那么到这里,ArrayList的动态扩容就讲解完毕了。
不过grow
方法中的代码还需要提示两点:
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
这段代码,确保了ArrayList动态扩容的下限!
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);
而这一段代码,确保了ArrayList动态扩容的上限!
/**
* 返回列表中元素的个数
* @return 返回列表中元素的个数
*/
public int size() { return size; }
/**
* 如果此列表不包含任何元素,则返回true
* @return 如果此列表不包含任何元素,则返回true
*/
public boolean isEmpty() { return size == 0; }
接下来这两段代码其实就很简单了,一个是获取ArrayList中元素的个数,也就是数组内元素的个数。另一个则是判断ArrayList是否为空,也就是数组内元素的个数是否为空。然后......就没了(*^ω^*)。
/**
* 如果此列表包含指定的元素,则返回true。
* 正式地说,当且仅当此列表包含至少一个元素e(o==null?e==null:o.equals (e))。
* @param o 被检查在此列表中存在的元素
* @return 如果此列表包含指定的元素,则返回true
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
接下来的这个contains
方法也是很简单的,就是在单纯的判断我们的ArrayList,也就是数组是否包含我们传进来的参数o。换人话就是,其目的就是检查是否包含指定的元素。不过下面我们将要说的是其内部的真实逻辑,也就是代码中的indexOf(o) >= 0
,这里是进行元素检查的核心部分。
/**
* 返回该列表中指定元素第一次出现的索引,
* 如果该列表不包含该元素,则返回-1。
* 正式地说,返回最低索引 i,使得(o==null?get(i)==null:o.equals(get(i))
* 如果没有这样的索引,则为-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;
}
/**
* 返回此列表中指定元素最后一次出现的索引,
* 如果此列表不包含该元素,则返回-1。
* 正式地说,返回最高索引 i,使得(o==null?get(i)==null:o.equals(get(i))
* 如果没有这样的索引,则为-1。
*/
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;
}
OK,现在我们到了解析contains
方法核心的部分了,不过这里我准备把indexOf
和lastIndexOf
放在一起说。因为一个找最头,一个找最尾,一起凑合凑合过得了。
那我们先开始indexOf
方法,毕竟它是contains
方法起作用的真正方法。
我们可以明确地从方法注释中获取到该方法返回的是指定元素出现的索引,也就是通常说的数组下标。而这个下标还是该元素第一次出现的地方。
那么我们从代码入手来看看其实现。
首先,它会对我们指定的元素进行判空
1). 如果为空(也就是说这里需要找到一个空参数),那么接下来就是一个for循环。而这个for循环其实就是为了遍历数组。我们可以看到这里的数组是由数组下标0
开始遍历,一直到ArrayList的元素个数size
为止。
接下来就是取出遍历到的当前元素,让其与null值做比对,如果比对成功,则直接返回该下标,否则就继续查找。如果上述都不满足,很遗憾就会返回-1。
2).如果不为空,那么接下来依旧是一个for循环。只不过这个for循环里面的判断条件是对传入元素和具体的数组项进行了equals
对比。如果符合了判断条件,也就是找到了指定的元素,就直接返回该下标,否则就继续查找。如果还是不满足,则返回-1。
那么在这里就可以先解释一下,为什么indexOf
方法是返回列表中指定元素第一次出现的索引呢,代码已经告诉我们了,因为我们是从数组的第一个元素开始遍历的,只要碰见就返回,碰不见,就没办法了......
到了这里,indexOf
方法就完毕了,那么我们开始说一下lastIndexOf
方法。lastIndexOf
这个方法无论是从方法名称命名来看还是从方法注释来看,这个方法的作用都是清清楚楚的。它也是返回指定元素出现的索引,也就是通常说的数组下标。而这个下标却是该元素最后一次出现的地方。
简单的看一下代码部分,lastIndexOf
和indexOf
的唯一区别就是遍历数组的起始位置,其他的,类似于条件,默认返回值(-1)都是一样的。
我们的lastIndexOf
的数组遍历是从ArrayList的元素个数size-1
开始的,一直到0
这个起始位置,也就是所谓的从后遍历。所以理所应当的就是返回此列表中指定元素最后一次出现的索引。
/**
* 返回此ArrayList实例的浅拷贝(元素本身不会被复制)。
* @return 这个ArrayList实例的拷贝
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// 只要我们实现了Cloneable接口,这里是不会发生这种错误
throw new InternalError(e);
}
}
这里我们介绍一下ArrayList里面的这个clone
方法。它不难理解,其实就是对原数组的一个拷贝,一个原elementData
的副本而已。
我们从代码入手的话不难发现最初它是利用了自己祖类的clone
方法进行了一次拷贝。但是其祖类java.lang.Object
的clone
是一种浅拷贝,所以此时声明的类型为ArrayList<?>
的v
的elementData
持有的依旧是原ArrayList的elementData
的引用,说白了,如果我们此时对这个v
进行操作,那么原ArrayList的elementData
内部的元素就会受到影响!
所以,这里它又用Arrays.copyOf
方法进行了一次数组的复制,并将其赋予现在的v
的elementData
。那么此时,这个v
的elementData
就是原ArrayList的elementData
的一个副本了。其不再是内存中同一个数组。所以说,这个时候我们再操作v
就不会影响原ArrayList的elementData
内部的元素(这句话说成“不会影响原ArrayList”也行,只不过不严谨而已 →_→)。
/**
* 返回一个数组,该数组按适当的顺序(从第一个元素到最后一个元素)包含列表中的所有元素。
* 返回的数组将是“安全的”,因为这个列表不维护对它的引用(换句话说,这个方法必须分配一
* 个新数组)。因此,调用者可以自由地修改返回的数组。
*
* 此方法充当着基于数组和基于集合的API之间的桥梁。
* @return 一个数组,包含列表中所有元素的正确顺序
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
我们接下来说的这个方法叫toArray
,秉承“絮絮叨叨”的原则还是简单的说一下。
这个方法其实注释已经说的很清楚了,这个注释是我按照原先的英文语义翻译过来的,原语义很清楚,翻译的也很清楚。它就是利用Arrays.copyOf
方法复制了一个数组并返回给调用方。而且人家说了,这个数组是安全的,因为这个列表不维护对它的引用。到这里其实也就没什么了。
但是有一点特别的是,你会发现,这个方法是不是和上面的clone
方法和trimToSize
方法相似呢?所以这里会有一个小的对比:
相同点:
1. 三者均为对数组的拷贝,且都是浅拷贝;
2. 三者均是通过Arrays.copyOf(elementData, size)
进行数组的复制;
不同点:
1. 对于操作数modCount
来说,trimToSize
方法会对操作数modCount
做+1
的操作,clone
方法会将新建的ArrayList中的操作数modCount
置0
,而toArray
方法则对操作数modCount
没有任何行为的修改;
2. 对于数组形式来说,trimToSize
方法是将复制好的数组又赋回给了ArrayList本身的数组elementData
,clone
方法是返回了一个ArrayList的对象,类型为Object
,而toArray
方法则是直接将复制好的数组进行返回,类型为Object[]
;
3.从操作程度来说,trimToSize
方法返回后进行操作是影响原有数组的,而clone
方法和toArray
方法返回后进行操作是不影响原有数组的。
4. 从出发点来说,trimToSize
方法是为了缩减数组大小开支,clone
方法是复制了一个ArrayList对象,同时也缩减了数组大小开支,但其目的是供调用者进行单独处理使用的。也就是说,经过clone
返回的数组其实是原数组的一个副本,可以随意修改而不影响原有的elementData
(举个例子,我想要减肥,trimToSize
是真正的当前的“我”进行了减肥,而clone
则是得到另一个“我”进行减肥,虽说都是减肥,然而从“观感”和“行为”上来看是不一样的)。那么接下来的toArray
方法则是在缩减数组大小开支后返回一个“安全的”新数组,我们可以更加灵活的去操控它。可以将toArray
方法理解为clone
方法的一个缩减,也就是直接将复制过的数组提前返回了。
那么到此,这个toArray
方法我们也就说完了。
/**
* 返回一个数组,该数组按适当的顺序(从第一个元素到最后一个元素)包含列表中的所有元素。
* 返回数组的运行时类型是指定数组的运行时类型。如果列表符合指定的数组,则返回该列表。
* 否则,将使用指定数组的运行时类型和此列表的大小分配新数组。
*
* 如果列表符合指定的数组的剩余空间(即,数组的元素比列表的元素多),那么数组中紧跟
* 在集合末尾的元素被设置为null(只有在调用方知道列表不包含任何空元素时,这才有助于
* 确定列表的长度)。
* @param 如果列表的元素足够大,则将其存储到其中的数组;否则,将为此分配相同运行时
* 类型的新数组
* @return 返回包含列表元素的数组
* @throws ArrayStoreException 如果指定数组的运行时类型不是此列表中每个元素的运
* 行时类型的超类型则抛出此异常
* @throws NullPointerException 如果指定的数组为空则抛出此异常
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
// 创建一个新的数组的运行时类型,但我写的是:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size) a[size] = null;
return a;
}
歇会儿回来哈。那么现在我们要说的一个方法是另一个toArray
方法,只不过这个是携带参数了的——toArray(T[] a)
。
废话不看段落:这个方法其实最好还是与上面的public Object[] toArray()
方法进行对比讲解。但是没有放在一起的原因主要是因为,之前的三个方法(trimToSize
、clone
、toArray
)的可对比性强度要高,而此时的这两个toArray
的可对比性显得要低些,所以,目前这个带有参数的toArray
方法我们是单独出来说明的。
还是老样子,上面的方法注释其实已经说明的.....我反正是一知半解,进行了原有的语义翻译反而越翻越乱,想加入自己的意思又怕搞乱原文。所以现在,直接干代码(*•̀ᴗ•́*)و ̑̑:
我们先看返回类型的格式,返回类型的格式是一个数组
,这个与无参toArray
方法一致,只不过这个有参的toArray
方法指定了返回数组的类型。那么我们再看参数,也是一个数组,而且类型与返回类型相同。那么我们再瞅一眼泛型级别,你会发现泛型级别是和返回类型和参数类型是一致的。那么这个时候我们就明白了这个方法的含义:利用当前指定泛型后的ArrayList的有参toArray
方法,传入一个符合原ArrayList泛型的数组,然后构造一个符合原ArrayList泛型的新数组且保留原有元素信息,之后,将其返回。再直白一些:将当前ArrayList转换为指定类型的数组。
方法意义解释完后我们看一下方法体部分。首先它会进行一次判断,将传入的数组a
的长度与当前ArrayList内的元素个数size
做一次对比。如果我们传入的数组长度比当前ArrayList内的元素个数要小,那么我们直接复制一个长度为ArrayList内的元素个数大小的数组就可以了。然后将这个数组返回,里面的内容还是原来的配方,还是原来的味道,只不过是利用Arrays.copyOf
方法进行了数组的缩容。这里必须举个例子了:
/**
* 请放心运行
* 但是这里写法不正确啊,仅仅是做示例用的!!!
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
arrayList.add("e");
String[] a = new String[]{"1", "2", "3"};
System.out.println("原ArrayList集合: " + arrayList + " 元素个数为: " + arrayList.size());
System.out.println("新建的参数a数组: " + Arrays.toString(a) + " 数组长度为: " + a.length);
String[] strings = arrayList.toArray(a);
System.out.println("执行arrayList.toArray(strings)中.................................");
System.out.println("返回的数组strings1: " + Arrays.toString(strings) + " 数组长度为: " + strings.length);
}
/*
* 输出结果:
* 原ArrayList集合: [a, b, c, d, e] 元素个数为: 5
* 新建的参数a数组: [1, 2, 3] 数组长度为: 3
* 执行arrayList.toArray(strings)中.................................
* 返回的数组strings1: [a, b, c, d, e] 数组长度为: 5
*/
OK,那么如果数组a
的长度大于了当前ArrayList内的元素个数size
呢?那么就利用System.arraycopy(elementData, 0, a, 0, size)
方法将原ArrayList的数组elementData
复制到我们传进来的这个a
数组里面(源数组为elementData
,目标数组为a
,都是从最开始的下标0
开始替换,长度为当前ArrayList内的元素个数size
)。即,把原elementData
挪到a
里面去。
其实与上面这一步相关的,还有接下来的这个判断。就是显式的判断数组a
的长度是否大于当前ArrayList内的元素个数size
。没有的话,直接将这个a
数组返回就行了。然而如果有的话,那么就把当前即将返回的数组a
的索引为size
位的值置为null。也就是我们原ArrayList的数组elementData
的最后一个元素的后一位(因为最后一位元素我们是用elementData[size-1]
表示的,所以这里应该是可以理解的)置为null。再来个例子啊:
/**
* 请放心运行
* 但是这里写法不正确啊,仅仅是做示例用的!!!
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
arrayList.add("e");
String[] a = new String[]{"1", "2", "3","4","5","6","7","8"};
System.out.println("原ArrayList集合: " + arrayList + " 元素个数为: " + arrayList.size());
System.out.println("新建的参数a数组: " + Arrays.toString(a) + " 数组长度为: " + a.length);
String[] strings = arrayList.toArray(a);
System.out.println("执行arrayList.toArray(strings)中.................................");
System.out.println("返回的数组strings1: " + Arrays.toString(strings) + " 数组长度为: " + strings.length);
}
/*
* 输出结果:
* 原ArrayList集合: [a, b, c, d, e] 元素个数为: 5
* 新建的参数a数组: [1, 2, 3, 4, 5, 6, 7, 8] 数组长度为: 8
* 执行arrayList.toArray(strings)中.................................
* 返回的数组strings1: [a, b, c, d, e, null, 7, 8] 数组长度为: 8
*/
我们来看这个例子,不用细看,只看输出结果。我们发现在执行完arrayList.toArray(strings)
这步操作后返回的数组是[a, b, c, d, e, null, 7, 8]
。
抛开null值不说,这里面的元素除了原ArrayList的数组elementData
的元素以外,还囊括了我们传入的a
数组的元素。这恰恰也证明了之前我们的分析——即,把原elementData
挪到a
里面去。那么这个参数a
到底干了啥呢?
其实,这个参数a
是起到了一个标尺的作用,你也可以理解为它在丈量数组。整个方法的目的是为了将原ArrayList由List类型转换为数组类型,但这种转换其实就是一种数组复制,而数组复制就需要一个合理的容量范围。我们不能单纯的利用Arrays.copyOf(elementData, size)
这个方法进行数组的复制,因为这样操作的话扩展性难免会收到约束,况且我们已经有了无参的toArray
方法。
我们在这一行行的逻辑中无不发现都是在利用a
的长度进行“丈量”测算。然后根据测算结果返回拥有合理容量的数组。不过这里面也会发生“扯egg”的现象,就是我们传入的数组远远超出了原ArrayList内的元素数量,导致返回的数组可能会非常的臃肿。上面的错误示例就恰恰说明了这一点。
我们来上一个正确的示例:
/**
* 这个更可以放心运行啊
* 这回是正确的!!!
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
arrayList.add("e");
System.out.println("原ArrayList集合: " + arrayList + " 元素个数为: " + arrayList.size());
String[] strings = arrayList.toArray(new String[0]);
System.out.println("执行arrayList.toArray(strings)中.................................");
System.out.println("返回的数组strings1: " + Arrays.toString(strings) + " 数组长度为: " + strings.length);
}
/*
* 输出结果:
* 原ArrayList集合: [a, b, c, d, e] 元素个数为: 5
* 执行arrayList.toArray(strings)中.................................
* 返回的数组strings1: [a, b, c, d, e] 数组长度为: 5
*/
这里面唯一的改动就是将我们之前需提前定义的a
数组替换为了直接声明:arrayList.toArray(new String[0]);
。即,隐式地触发有参toArray
方法的Arrays.copyOf(elementData, size, a.getClass());
,避免出现之前带有null值的非预期结果。
至此,这个toArray(T[] a)
方法我们就说完了。
ArrayList的增删改查系重要方法的解析:
到现在,我们开始进入ArrayList的增删改查系重要方法的解析,当然这里可能会涉及到另一些底层代码,所以以下出现的可能不仅仅是通常意义上的add
、remove
、set
、get
等特定方法。也会出现诸如elementData
、rangeCheckForAdd
、listIterator
等一系列功能方法。目的就是不单单的隔离这些相互作用的功能,他们可能是息息相关的。
// 位置访问操作
/**
* 返回指定索引的元素
* @param index 指定的索引
*/
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
这里先说一个简单的方法,
好,现在我们先来一个简单的elementData
方法。不过这个方法确实整个ArrayList可以进行删、改、查的重要方法。
这个方法非常的简约,即使不阅读注释(那注释也是我自己加的(>ω<)),也能从代码中明白这个elementData
方法的含义。其含义就是获取数组中指定索引的元素。
唯一可以能拿出来说的也就是它的访问修饰符,也就是default
默认类型,该访问修饰符指出了我们的这个elementData
方法仅仅可以由类内部和本包java.util
下的类进行访问。解析完毕~
/**
* 检查给定的索引是否在范围之内。如果不是的话,则抛出适当的运行时异常。
* 这个方法不检查索引是否为负数:因为它总是在数组访问之前使用,如果索引
* 是负的,则会抛出ArrayIndexOutOfBoundsException异常。
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 由add和addAll使用的rangeCheck的一个版本。
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 构造IndexOutOfBoundsException详细信息。
* 在错误处理代码的许多可能的重构中,这种“概括”在服务器和客户端虚拟机上执行得最好。
*/
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
这里,我们一下三联三个方法rangeCheck
、rangeCheckForAdd
、outOfBoundsMsg
。为什么要先介绍这三个方法呢?因为这三个方法涉及了ArrayList的增删改查系列方法中最重要的一环,即,检查索引范围。
这里我们不一一讲解,因为代码过于简练,通俗易懂,所以就不说的那么繁琐了。我们仅仅区分一下就行。
首先,rangeCheck
方法和rangeCheckForAdd
方法两个都是对索引范围进行检查的方法。唯独rangeCheck
方法是不检查索引是否为负数的。通过其注释说明,如果索引为负数,那么就会抛出ArrayIndexOutOfBoundsException
异常,所以就不进行检查了。
其次,rangeCheck
方法是由ArrayList中的get(int index)
、set(int index, E element)
、remove(int index)
这三个方法直接调用,涵盖了ArrayList的查、改、删三大功能。然后就是我们的rangeCheckForAdd
方法,该方法是由ArrayList中的add(int index, E element)
、addAll(int index, Collection<? extends E> c)
两个方法直接调用,涵盖了ArrayList的增的功能。不过它与rangeCheck
方法不同的是,它对索引是否为负进行了判断。
最后,就是我们的outOfBoundsMsg
方法。这个方法是前两个方法在抛出IndexOutOfBoundsException
异常时进行信息提示的方法。其实它就是一个简单的字符串拼接。把超出范围的索引打印出来,把当前的元素个数打印出来就没了。
这里有一个细节的问题,那就是rangeCheck
方法判断范围的时候,是把size
值包含进去的。而rangeCheckForAdd
是不包含size
值的。这其实就说明了为什么这两个方法看似一样,实则不一样的原因。
那就是调用rangeCheck
方法的查、改、删这三个功能是不能取size这个值的,因为size这个值表示的是元素的个数,而最坏情况它还能表示数组的长度,也就是整个数组都被填满。而我们能查、改、删的位置最大也就是size-1
。所以,当索引位变为size
的时候,是超出数组范围的,这也就是为什么会抛出IndexOutOfBoundsException
异常了。
而调用rangeCheckForAdd
方法的则是增这个功能。具体增这个功能其实就是add(int index, E element)
和addAll(int index, Collection<? extends E> c)
这两个方法。他们都是指定索引位置进行添加。那么我们在向数组内添加元素的时候是允许在数组为满的情况下(这种情况非常少非常少)再次添加元素的。也就是最坏的情况,是将元素添加至数组的最后一个元素的后一个位置。而这个最后一个元素的位置的下标为size-1
,其后一个位置就是size
。
提示:别忘了我们后续还会扩容的。
ArrayList的增删改查方法的解析:
从现在开始,我们进入到了增删改查等方法的讲解。经过上面一系列的讲解,可能发现,其实我们经常认为的重点方法,只不过是前面一些方法的整合。
第一,从访问修饰符来看,这个ArrayList提供的这些增删改查方法全都是public可以由我们的对象直接访问的方法。
第二,从整体代码细节来看,这些方法其实也是之前那些功能性方法的一个整合,所以这里面就涉及了一个问题,也就是出发点的问题。那就是我们看似重要的方法(增删改查)可能仅仅是最普通的,而那些边边角角的方法(上面提到过的)才是最重要的。
废话不看段落:这里面的区别就在于那个所谓的“出发点”问题。平常我们看些博客,总是先从一系列的增删改查方法进行一步一步调试,一步一步进入,然后一步一步解析。然而这些所谓的入口我却称之它为“表层代码”,这些“表层代码”就是一种“拿来主义”,拿来就用。当然,这个拿来就用我说的是增删改查这些方法本身的内部逻辑,并不是指他们的调用者。因为这些方法本身就是为了开发者拿来即用的。
闲言碎语不要讲,开始开始~~~
查:
/**
* 返回列表中指定索引的元素
* @param index 要返回元素的索引
* @return 位于列表中指定位置的元素
* @throws 如果索引超出范围(index< 0||index>= size())
* 则抛出IndexOutOfBoundsException异常
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
这个方法就是我们ArrayList的查询方法,它会返回列表中指定索引的元素。这个说法好纠结,换个说法就是,返回数组中指定位置的元素。当我们拿到指定的索引值后,会先对这个索引进行范围检查,如果该索引大于或等于当前元素的个数,那么就会抛出IndexOutOfBoundsException
异常,否则直接返回该数组下标所匹配的元素即可。
改:
/**
* 将列表中指定位置的元素替换为指定元素。
* @param index 要替换的元素的索引
* @param element 存储在指定位置的元素
* @return 返回替换之前位于指定位置的元素
* @throws
* 1.如果索引超出范围(index< 0||index>= size())则抛出IndexOutOfBoundsException异常
* 2.如果此列表不支持set操作则抛出UnsupportedOperationException异常
* 3.如果指定元素的类阻止将其添加到此列表中则抛出ClassCastException异常
* 4.如果指定的元素为空,并且此列表不允许空元素则抛出NullPointerException异常
* 5.如果指定元素的某些属性阻止将其添加到此列表中则抛出IllegalArgumentException异常
*/
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
这个方法就是我们ArrayList的修改方法,它会将列表中指定位置的元素替换为指定元素。也就是说替换当前数组中指定位置的元素,并将该位置之前的元素返回。当我们拿到指定的索引值后,会先对这个索引进行范围检查。如果通过了检查则先声明一个值,我们称之为“旧值”oldValue
,这个oldValue
就是我们将要返回的那个之前的元素。
我们声明完毕后还需要从当前数组中检索出这个值并将其赋予oldValue
,到这里,我们完成了上述代码的第二行操作。
接着,就是修改(替换)的步骤。很明显,这里直接对数组中指定位置的地方进行了赋值操作,将上层传过来的新值element
替换了这里的旧值。那么这里,就是ArrayList修改元素的本质。
最后,再将获取到的“旧值”oldValue
返回就可以了。
增:
/**
* 将指定的元素追加到此列表的末尾
* @param e 需要添加到此列表中的元素
* @return 返回是否添加成功的标志
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 操作数+1
elementData[size++] = e;
return true;
}
这个方法就是我们ArrayList的添加方法,它会将指定的元素追加到此列表的末尾。也就是说把指定的元素放到当前数组最后一个元素的后面。
我们来看第一行。当我们传入指定的元素时,并没有着急的将其放置到数组中。而是先进行确保数组容量,而这个ensureCapacityInternal
方法我们之前是说过的,代码中出现的size+1
就是这个数组所需的最小容量minCapacity
。那么为什么最小容量minCapacity
是size+1
呢?因为这个size
就是当前数组的元素个数,而我们添加元素时,首先需要确认的是我们这个数组能否还可以容纳这一个元素。所以这里就是原元素个数+待添加元素个数
。这样,把这个值传入到ensureCapacityInternal
方法中进行运算即可。该不动的不动,该扩容的扩容。
当我们把容量的事儿算清楚之后我们就可以添加元素了,怎么添加呢?其实就是在该数组最后一个元素的后面添加就可以了。那么这最后一个元素在哪里呢?其实就是当前元素的个数
,也就是size
,因为我们的数组是从数组下标为0
的地方开始的,所以最后一个元素的索引其实是size-1
,那么后一个就是size-1+1
,也就是size
这个值。
但是我们看到代码中数组的索引位是size++
,这个还是好理解的。注意,这个自增操作是后++
,也就是当前的size
如果是5的话,此时这个size++
不会是6,它依旧还是5。只不过这个size++
值已经在内存中进行了计算,也就是变为了6
。而这个6
表示的则是元素的个数。如果还不好理解的话,可以参考下列代码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 操作数+1
elementData[size] = e;
size++;
return true;
}
也就是换了方式而已,不过可能看起来更直观而已(*^ω^*)。
/**
* 将指定元素插入到列表中的指定位置。将当前位于该位置的元素(如果有)和
* 任何后续的元素向右移动(将一个元素添加到它们的索引中)。
* @param index 要插入指定元素的索引
* @param element 要插入的元素
* @throws 如果索引超出范围(index< 0||index>size())则抛出IndexOutOfBoundsException异常
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // 操作数+1
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
这个方法也是我们ArrayList的添加方法,只不过它会将指定元素插入到列表中的指定位置。也就是说把指定的元素放到当前数组中指定的位置上。
这个方法的特点注释也说的很明白了,就是在插入的时候,会将插入点位置的元素连同后面的元素一齐往后(右)移动。其实就是给新来的元素“挪坑”。
不过我们还是一步一步的往下看吧。
我们来看第一行。这里拿到指定的索引之后,先对其范围进行了判断,判断这个索引是否安全。这个判断是很有必要的,因为我们的插入操作是不可能允许你随意的在数组中的不合理的地方进行插入操作的。
紧接着,第二行开始进行数组容量的检测。依旧是把minCapacity指定为size+1
,这一步我们上面的方法已经解释过了,这里不做赘述了。
第三步,这才是我们插入操作的重点。我们发现,这里做了一步利用System.arraycopy
方法进行数组复制的操作。其中源数组和目标数组都是同一个elementData
,也就表示我们还是在原来的基础上进行了复制。那么具体复制了哪些内容呢?
其中System.arraycopy
的第二个参数表示了源数组要复制的起始位置,而第四个参数表示了目的数组放置的起始位置,最后一个参数表示了需要复制的长度。这三个地方分别对应的值就是代码中的index
、index+1
、size-index
。
搞清楚了这个System.arraycopy
的作用,我们就可以解答这步操作的作用了。
首先,我有两个数组,一个是elementData
,另一个,还是elementData
。
然后,需要复制源数组的起始位置是我们指定的位置index
,需要放置到目标数组的位置是我们指定的位置的后一位,也可以理解为index
所对应的元素在数组中的右一格,也就是index+1
。那么复制多少呢?代码告诉我们要复制size-index
个,也就是需要复制从当前指定索引的元素算起且包含该元素并一直到当前数组的最后一个元素的长度。
这样,我们就明白了这一层的含义,它就是要把我们指定位置的元素以及其后的所有元素向后(右)挪一格,为了就是把指定的位置空出来。
空出来干嘛呢?这就到了我们的第四行,那就是把我们要插入的元素赋予这个空出来的位置。也就是插入进去。这样,我们的元素就插入到指定的位置里面了。
那么到了第五步尾声,把我们当前数组的元素个数size
自增1,告诉我们数组里的的元素又多了一个。当然我把话述改为当前ArrayList里面的元素多了一个也是可以的。
/**
* 将指定集合中的所有元素追加到此列表的末尾,它们按照指定集合的迭代器的顺序进行添加。
* 如果在操作过程中修改了指定的集合,则此操作的行为未定义(这意味着,如果指定的集合
* 是这个列表,并且这个列表不是空的,则此调用的行为是未定义的。)
* @param c 被添加到该列表的集合
* @return 如果此列表的调用结果更改了,则返回添加成功
* @throws 如果指定的集合为空,则抛出NullPointerException异常
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // 操作数+1
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
接下来我们介绍的是另一类行为的添加方法,即,向该ArrayList
内添加另一组集合。
废话不看段落:经过上一个方法的解析,这里我们的流程就显得通顺多了。也就是说这里就不再一步一步的进行分析,因为不需要重复add(int index, E element)
方法的解析过程。特意说一下的原因不是因为懒得写,而是到了这一步,相信代码阅读应该已经很顺畅了。如果还有难度,那么就参照add(int index, E element)
方法的解析过程。那现在我们开始:
好,这里需要注意的是,传入的参数不再是某个指定的元素了,而是一个集合。那么这个集合有什么特点呢?答,通过参数泛型可观察到该集合是当前ArrayList
集合的子类集合,或其本类集合(<? extends E>
)。那么如何把这个集合添加进我们现在的ArrayList
内呢?我们来看代码。
第一步,它将这个传入的集合c
转换成了一个Object
类型的数组a
;
第二步,计算集合a
的长度,也就是之后新增元素个数。
第三步,保证当前数组的内部容量,根据容量计算情况进行扩容操作。其中minCapacity
指定为当前元素个数加上新增元素个数size + numNew
。注意:这里不再是size+1
,size+1
代表的是添加一个元素。
第四步,数组的复制。其中源数组为a
,目标数组是当前数组elementData
。源数组复制的起始位置是从索引0开始,复制numNew
个,也就是全部复制。之后,复制到目标数组elementData
中,而复制到的位置则是从size
位上开始一直往后。这个size
位就是我们当前数组elementData
最后一个元素的后一位(因为数组索引是从0
开始,所以最后一位元素的索引是index => size-1
,那么它的后一位就是index => (size-1)+1 => size
)。
第五步,更新当前数组的元素个数size += numNew
。
最后,判断新增元素个数是否不为0
,并将判断结果返回。其实这一步就是证明是否添加了元素,如果正常添加了元素,其结果肯定不为0
,否则就是没有添加。
/**
* 从指定位置开始,将指定集合中的所有元素插入此列表。将当前位于该位置的元素(如果有)
* 和任何后续元素向右移动(增加它们的索引)。新元素将按照指定集合的迭代器顺序的出现在
* 此列表中。
* @param index 从指定集合中插入第一个元素的索引
* @param c 被添加到该列表的集合
* @return 如果此列表的调用结果更改了,则返回添加成功
* @throws 如果索引超出范围(索引<0||索引>size())则抛出IndexOutOfBoundsException异常
* @throws 如果指定的集合为空,则抛出NullPointerException异常
*/
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
接下来的这一个方法也是向我们的ArrayList
内添加另一组集合。只不过这里指定了插入位置index
。
那么这里参数就不用细说了,还是当前ArrayList
集合的子类集合,或其本类集合(<? extends E>
)。我们直接来看代码干了什么。
第一步,检查索引范围。这一步毋庸置疑是必须的,因为在add(int index, E element)
这个方法中也同样运用了rangeCheckForAdd
方法。毕竟我们还是需要检查一下传进的索引是否是合法的;
第二步,依旧是将这个传入的集合c
转换成了一个Object
类型的数组a
;
第三步,计算集合a
的长度,也就是之后新增元素个数。
第四步,保证当前数组的内部容量,根据容量计算情况进行扩容操作。其中minCapacity
指定为当前元素个数加上新增元素个数size + numNew
。
第五步,从这里开始,就要计算移动元素的个数了。这里移动元素的个数是通过size-index
计算出来的。这个很简单,其实就是从当前索引且包含当前索引上的元素并一直到最后的元素个数,由此来得出需要移动元素的个数numMoved
;
第六步,对移动元素的个数numMoved
做判断,问是否大于了0
。其实就是在问需不需要挪动元素;
第七步,我们来看不需要的时候怎么办。不需要的话其实就是在尾部添加集合,因为只有在尾部添加才不需要挪动元素。这就和上一个方法addAll(Collection<? extends E> c)
一样,直接将数组a
复制到当前数组elementData
的尾部就可以了。不过需要注意的是,这里目标数组的起始位置不再是size
,而是我们传入的索引index
;
第八步,也就是需要挪动元素呢?如果需要挪动元素的话我们看到,此时源数组和目标数组都是当前的elementData
,源数组的起始位置是传入的index
,也就是指定插入的位置。需要挪动的元素个数也是在第五步计算出来的numMoved
。只不过我们把这些东西复制到哪里去呢?答案就是第四个参数index + numNew
。
这个index + numNew
就是当前索引加上新增元素的个数。说白了,就是让我们当前的elementData
数组空出numNew
个位置,给新增的元素腾出位置。到这一步,这才仅仅是腾出了位置,将原有位置的元素向后挪动了。
挪动完毕后,才开始正式的插入。这一步就是我们现在的第七步,将数组a
复制到当前数组elementData
指定的位置index
中。到这时候,才完成了元素的添加。
第九步,依旧是更新当前数组的元素个数size += numNew
;
最后,判断新增元素个数是否不为0
,并将判断结果返回。也就是判断有没有正确的添加元素。
删:
/**
* 删除列表中指定位置的元素。
* 将任何后续元素向左移动(从它们的索引中减去1)。
* @param index 要删除的元素的索引
* @return 返回从列表中删除的元素
* @throws
* 1.如果索引超出范围(index< 0||index>= size())则抛出IndexOutOfBoundsException异常
* 2.如果此列表不支持删除操作则抛出UnsupportedOperationException异常
*/
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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
这个方法就是我们ArrayList的删除方法,它将会删除列表中指定位置的元素并将之前存在于此的元素返回。
这个方法的特点注释也说的很明白了,就是在删除的时候,会将插入点位置的元素连同后面的元素一齐往前(左)移动。其实就是“补坑”。补得就是被删元素的那个“坑”。
我们来看第一行。这里拿到指定的索引之后,先对其范围进行了判断,判断这个索引是否安全。紧接着第二行则是对操作数modCount
做自增操作。
第三行它做了一个和set
方法类似的操作,就是声明“旧值”oldValue
,从当前数组中取出指定索引位置的元素并将其赋予“旧值”oldValue
。这个oldValue
就是我们将要返回的那个被删的元素。
第四行中,它声明了一个叫做numMoved
的变量,那么这个变量是什么呢?我们看它是如何计算的。
代码显示,numMoved
的计算公式是:size-index-1
。什么意思呢?其实就是后续需要挪动元素的个数。
我们仔细看,这个方法的作用是删除,前提也说了,删除后需要将删除元素的后面的元素整体往前挪,那么这里就涉及了一个挪多少的问题。显而易见是全部挪动。那么,我们就需要计算一下了。首先size
代表的元素的个数,其次,index
代表了数组下标。但是,我们的数组下标是以0
位开始的,所以,要想让index
代表第几个元素,那就得用index+1
才能表示。所以,我们得到了最后的计算过程,即,size-(index+1)
。去掉括号,就是size-index-1
,即我们需要向前挪动size-index-1
个元素。
当前,这里的运算过程的入手点是从“元素个数”出发进行验算的,如果从“下标”出发就变成了size-1-index
表示法了。不过,殊途同归,他们都是用来表示挪动元素个数numMoved
的。
接下来,到了第五行。它又对numMoved
进行了一次判断,它在问我们挪动的元素个数是否大于0。其实他在问的是,我们是否删除的是最后一个元素。
所以,我们第六行和第七行一起说。如果我们删除的是最后一个元素的话,那么就直接按照第七行表示的将最后一个元素格置为null即可,然后整个元素个数自减。否则,就是第六行所说的,进行数组赋值(不过这里需要强调的是,第七行(最后一位置为null)可是永远执行的,跳过的步骤仅仅是第六行的数组复制而已。)。
OK,秉承事无巨细的元素,我再絮叨一下第六行数组的复制。因为这才是整个删除过程的核心。我们运用System.arraycopy
方法之前确定了源数组elementData
与目标数组elementData
,其实就是它自己。那么从源数组的什么地方开始复制呢?答,就从我们删除了的元素的后一位开始复制,即,index+1
。那么复制到目标数组的哪里呢?答,复制到被删除了的那个元素的位置,以那里为起点,即,index
。那么复制多少呢?答,复制被删元素之后的所有元素,即,size-index-1=numMoved
个。这样,整个ArrayList的删除操作就完成了。
当然,不要忘了第七行的置null操作和元素个数size
的自减操作就可以了。
第八行,将“旧值”oldValue
返回,这一步就不说了。
废话不看段落:说个这里面有意思的,remove(int index)
方法针对elementData
的操作用了--size
;add(E e)
方法针对elementData
的操作用了size++
;add(int index, E element)
方法针对elementData
的操作用了size;size++;
一来二去,各有姻缘,三三两两,难解难分(ಥ_ಥ)。
/**
* 私有的移除方法,该方法跳过边界检查且不返回被移除的值。
*/
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
}
接下来我们先介绍一个简单的方法,它的名称是“快速移除”。怎么个快速移除呢?其实它的注释已经说的很清楚了,就是对索引不做范围检查了,同时,也不会将删除的元素进行返回。
其整个代码的核心流程和我们之前介绍的remove(int index)
方法其实是一模一样的。依旧经历了操作数自增->计算需移动元素个数->复制数组->数组最后一位置null。
这个方法可说的其实并不是这些,真正可以讲解的:
1. 其调用方法remove(Object o)
。fastRemove(int index)
的唯一调用者就是remove(Object o)
,除此无他;
2.为什么不需要索引的范围检查和返回被删除元素(这个放到下面讲。稍微提一下,能调到这个方法的索引都是合法索引)。
/**
* 从该列表中删除指定元素的第一个匹配项(如果存在)。如果列表不包含该元素,它将保持不变。
* 正式地说,删除索引i最低的元素,使(o==null?==null:o.equals(get(i))(如果存在这样一
* 个元素)。
* 如果该列表包含指定的元素,则返回true(如果该列表由于调用而更改,则返回true)。
* @param o 将从此列表中删除的元素(如果存在)
* @return 如果此列表包含指定的元素,则为真
*/
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;
}
到这里我们开始讲解ArrayList的第二种删除用法,具体的信息也能从方法注释中摘取到。它会从该列表中删除指定元素的第一个匹配项。也就是说该方法会根据元素进行删除,只不过删除的是第一次出现的那个。
这个方法的讲解就不能以行为讲解单位了,而应该以块。这样就分为了判断、循环、返回。
1. 第一块我们先来看返回。我们不说别的地方的return语句,就看最后一行。你会发现这个remove(Object o)
方法是默认返回false。意味着只要前面的条件不成立我这儿无论如何也要返回false,告诉程序删除失败。这是这个方法的基调,即,默认删除失败。
2. 第二块我们看判断。这里对传入的值进行了区分,一种为null
,另一种为其他形式的对象
。因为这里涉及到了不同值会有不同的判断方式,所以要加以区分。
3. 最后第三块,其实是最明显的那一块——循环。我们发现这两个循环都是在遍历当前数组。当然,这种遍历是从低位开始遍历,也就是从索引为0
的地方开始遍历。而唯一不同的是循环体内的判断条件。
如果我们需要删除null
,也就是我们传进来的参数是nul
,那么就会不断的遍历数组中每一位的参数是否为nul
,即,elementData[index] == null
。一旦发现就调用fastRemove(int index)
方法执行删除逻辑(数组复制),然后返回true
即可。
那么如果我们删除的是其他类型的数据呢?其实它也是遍历数组的每一位进行对比。不过不是之前的==
对比,而是对象本身的
equals
方法进行对比,即,o.equals(elementData[index])
。一旦发现也开始调用fastRemove(int index)
方法执行删除逻辑(数组复制),然后返回true
即可。
现在我们这个方法就解析完毕了。接下来我们说一个狠的!
/**
* 从列表中删除所有元素。该调用返回后,列表将为空。
*/
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
这个狠的方法就是上面的clear
方法。从注释上来看,它会删除数组中所有元素。我们从代码看的话也是很简单的。
首先操作数modCount
做自增操作。然后就开始遍历整个数组。将里面的每一个元素都置为null
。最后将当前数组元素个数size
修改为0
即可。
一定要记住,这个方法,只不过是将ArrayList
内部元素全部清空,其本身还是可以使用的。举个例子:
/**
* 放心运行
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList.clear();
arrayList.add("b");
System.out.println(arrayList);
}
/*
* 输出结果:
* [b]
*/
你看它这里是可以运行的。为什么要举这个例子呢,就是因为我们要区别ArrayList
的清空操作。一种清空操作就是我们的这个clear
方法,再一种是将整个ArrayList
置为null
。这么置null
操作可就把当前的ArrayList
进行了GC
操作,再次调用的话就会出现空指针异常NullPointerException
了。来个对比例子:
/**
* 不放心运行
*/
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("a");
arrayList = null;
arrayList.add("b");
System.out.println(arrayList);
}
/*
* 输出结果:
* Exception in thread "main" java.lang.NullPointerException
*/
所以这块儿要特意区分ArrayList
的清空操作。
/**
* 从该列表中删除其索引包含在fromIndex和toIndex之间的所有元素。将任何后续元素
* 向左移动(减少它们的索引)。这个调用通过(toIndex - fromIndex)元素缩短列表。
* (如果toIndex==fromIndex,则此操作无效)。
* @throws 如果fromIndex或toIndex超出范围(fromIndex<0 ||
* fromIndex>=size() ||
* toIndex>size() ||
* toIndex<fromIndex)
* 则抛出IndexOutOfBoundsException异常
*/
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
接下来我们介绍的这个删除方法比较特殊,它会删除指定范围内的元素。从formIndex
开始一直到toIndex
结束。它是一个左闭右开区间,也就是说这个toIndex
位置的元素是不会删除的。
那么,它所特殊的点不仅仅这一个,我们先把代码分析完,然后再来说说它特殊的另一个点。好我们开始分析代码:
第一步,操作数modCount
做自增操作;
第二步,计算需要挪动的元素。这里还是说一下,不要忘了,删除元素始终是需要将后续的元素填补回来的,也就是“占坑”。这里需要挪动的元素numMoved
是通过size - toIndex
计算出来的。其中size
代表了当前数组中元素的个数,而toIndex
则是删除范围内的右边界(当然并不包括它)。这句其实可以解释成toIndex
及其自身以后的所有元素个数。这样是不是理解就方便些了?
第三步,这里是所谓删除的核心。所谓的“删除”无非就是覆盖了当前范围内的元素。你看,这一步数组复制已经说的很明确了。源数组和目标数组都是其本身elementData
,从源数组的toIndex
位置开始复制,一直到最后一个元素。然后复制到哪里呢?复制到目标数组elementData
的fromIndex
位置上,因为你要从这里删除嘛,所以就覆盖掉这里的每一个元素,也就是将toIndex
及其自身以后的所有元素进行前移操作。
这里有一个问题,就是你挪动了以后就完成整个操作了吗?不一定吧?因为这里仅仅是复制,也就是说数组中原位置上的元素换了另外一个元素而已,其他尾部的元素还是保留了下来。那么接下来我们开始清空这些遗留下的数据。
第四步,开始计算执行了“删除”操作后该数组的最新元素个数newSize
(其实就是剩下多少个元素)。我们也看到这个newSize
是通过size - (toIndex-fromIndex)
就算出来的。这个toIndex-fromIndex
其实就是删除了多少个元素,那么我们剩下多少个元素呢?是不是得用原先元素的个数减去我们删了的?所以就是size - (toIndex-fromIndex)
从而得到了newSize
。注意:这里这个newSize
不光光是代表了当前数组的最新元素个数,它也代表了我们当前数组elementData
的最后一位索引的后一位(最后一位索引位是newSize-1
)。**
第五步,我们开始删除从newSize
索引位开始,一直到原先数组最后一位元素索引之间的数据。即,利用这个for循环开始将新数组所对应的索引下的值置为null
;
最后,将新元素的个数newSize
覆盖之前的size
即可。
到此,代码分析就完毕了。剩下的就是我们所谓的这个removeRange(int fromIndex, int toIndex)
方法的另一个特点,为什么,它会由protected
访问修饰符修饰。
我们不难发现,这个方法是由protected
访问修饰符修饰修饰的。这个修饰符限定了其只能由类内部、本包、子类来进行访问,外部包是无法访问到的。这是为什么呢?
我们仔细跟踪里一下这个方法的引用,发现其仅仅由5个类在对其使用。
到这里,我们给出我们的结论,其实,就是为了减少冗余,所以这个方法并不对外进行开放。
/**
* 从此列表中移除指定集合中包含的所有元素。
* @param c 包含要从此列表中删除的元素的集合
* @return 如果此列表因调用而更改,则为真
* @throws 如果此列表中的元素的类与指定的集合不兼容则抛出ClassCastException异常
* @throws 如果此列表包含空元素,而指定的集合不允许空元素,或者指定的集合为空则抛
* 出NullPointerException异常
*/
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
/**
* 仅保留此列表中包含在指定集合中的元素。换句话说,从这个列表中删除指定集合中不包
* 含的所有元素。
* @param c 要保留在此列表中的元素的集合
* @return 如果此列表因调用而更改,则为真
* @throws 如果此列表中的元素的类与指定的集合不兼容则抛出ClassCastException异常
* @throws如果此列表包含空元素,而指定的集合不允许空元素,或者指定的集合为空则抛
* 出NullPointerException异常
* @throws如果此列表不支持retainAll操作则抛出UnsupportedOperationException异常
*/
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
/**
* 批量删除或保留指定集合内的元素
* @param c 指定删除或保留的集合
* @param complement 是否删除标记
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
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;
}
到这里,恭喜,我们即将完成所有ArrayList
的删除操作解析。这里我们将一连介绍上面3个方法。因为这三个方法是息息相关的,可以理解为一种方法的衍生。而这个“被衍生”的方法就是上面代码段中最后的这一个batchRemove(Collection<?> c, boolean complement)
方法。
这个方法将会被removeAll(Collection<?> c)
和retainAll(Collection<?> c)
两个方法调用。而且我们从这两个代码中可以看到,唯一的区别就是在调用batchRemove(Collection<?> c, boolean complement)
方法时所传的第二个参数不一致而已,一个false
,另一个true
。
所以我们这里简单说一下removeAll(Collection<?> c)
和retainAll(Collection<?> c)
两个方法,然后着重分析batchRemove(Collection<?> c, boolean complement)
。
首先,功能方面:removeAll(Collection<?> c)
方法表示移除当前数组elementData
内指定集合c
中的元素;retainAll(Collection<?> c)
方法则表示保留当前数组elementData
内指定集合c
中的元素,其他的元素则删除。这两个方法的功能是相反的。
其次,步骤方面:都会对传入的集合c
进行非空判断,之后开始调用batchRemove(Collection<?> c, boolean complement)
方法。其中,removeAll(Collection<?> c)
方法传入参数false
,retainAll(Collection<?> c)
方法传入参数true
。
到此,removeAll(Collection<?> c)
和retainAll(Collection<?> c)
我们就解释完毕了,最主要的batchRemove(Collection<?> c, boolean complement)
方法我们来正式讲解。
首先,我们来看这个方法的参数。第一个参数c
代表了我们传入的集合,第二个参数complement
代表了我们这个传入的集合是删除还被被保留。可能到这里还是不太清楚,我们继续.......不过一定要记住,删除方法所传参数complement
为false
,而保留方法所传参数complement
为true
。
其次,我们看到代码第一行,就把当前的数组elementData
赋予了一个被final
修饰的另一个elementData
。往后的一切操作都是对这个新的elementData
的操作。我们可以理解其为是对原有数组的一个复制。
再次,到了下一步,这里声明了两个int
类型的变量r
和w
,并初始化为0
(这两个参数的含义下面说)。然后又声明了一个boolean
类型的变量modified
,并初始化为false
。这个modified
的含义其实就是判断我们的程序是否正确执行,也就是该方法的调用方的注释所写的——如果此列表因调用而更改,则为真。所以一旦程序删除或保留了元素,则将其更改为true
,否则,就是默认的false
。
接下来我们来看这里的循环操作:
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
这个循环操作被包围在了try
块当中,我们先不分析这里的操作是什么,我们就看r
和w
的位置。显而易见,这两个变量都是在数组elementData
的中括号内~明白了,这俩变量就是数组索引。
这个时候我们开始解释这个循环在干什么:
我们看到,for循环的第二参数为r < size
,size
是当前数组的元素个数。再结合if语句中elementData[r]
我们就能得知,它是在遍历我们当前的数组elementData
。
然后,这个if语句在判断什么呢?第一,它在判断我们传入的集合c
里面是否包含遍历数组elementData
所对应索引的元素。第二,将第一判断的结果与complement
参数进行比对,之后比对成功才能执行下一步操作。所以,这个complement
参数是什么就至关重要了。
我们之前说过,removeAll(Collection<?> c)
方法所传参数complement
为false
,而retainAll(Collection<?> c)
方法所传参数complement
为true
。那么我们这里分别进行对比解说:
1. complement
为false
,即删除操作:
如果我们当前的集合c
含有所遍历出的元素,即为true
,但是complement
为false
。那么这个时候,if判断不成立,下方的代码是不执行的。
如果我们当前的集合c
不含有所遍历出的元素,即为false
,同时complement
为false
。这个时候,if判断成立,则执行下面的代码elementData[w++] = elementData[r];
elementData[w++] = elementData[r];
表示将我们遍历出的元素从数组elementData
第一位开始进行存储(因为w
初始化为0
),也可以叫覆盖,也可以叫替换。之后w
进行自增,也就是索引+1,向数组的后一位挪动,准备下一次操作。
这样循环往复,你看,我们是不是把非c
集合内的元素都重新存入了elementData
,而c
集合内的元素就全部跳过不管了。这也就是为什么当complement
为false
时产生的效果为删除。简单来说就是,你含有,我不要,你不含有,我就要。
2. complement
为true
,即保留操作:
如果我们当前的集合c
含有所遍历出的元素,即为true
,同时complement
为true
。这个时候,if判断成立,则执行下面的代码elementData[w++] = elementData[r];
elementData[w++] = elementData[r];
表示将我们遍历出的元素从数组elementData
第一位开始进行存储(因为w
初始化为0
),也可以叫覆盖,也可以叫替换。之后w
进行自增,也就是索引+1,向数组的后一位挪动,准备下一次操作。
如果我们当前的集合c
不含有所遍历出的元素,即为false
,但是complement
为true
。那么这个时候,if判断不成立,下方的代码是不执行的。
又这样循环往复,你看,我们是不是把c
集合内的元素都重新存入了elementData
,而非c
集合内的元素就全部跳过不管了。这也就是为什么当complement
为true
时产生的效果为保留。简单来说就是,你含有,我就要,你不含有,我不要。
那么到这里是不是所有操作都完成了呢?并不是。要知道,这仅仅就是复制了部分元素到数组elementData
中指定的位置上,其原有位置的元素可能被覆盖也可能依旧是原先的元素。到这里你可能就明白了,我们还有一步,就是清空这些多余出来的元素。
好,我们来看看如何去清空这些多余的元素。
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;
}
}
这里我们看,相应的操作完成后,也就是相应的元素处理完成之后,它下面写了一个finally代码块,说明这里的代码是必须执行的。那我们就来看看这个必须执行的代码是怎么回事儿(这里面肯定就是处理多余元素的代码,因为这里再不处理,代码可就执行完毕了)。
注意啊,重难点开始了!我们开始解析这里的代码。
不难发现,这个finally代码块里面有两个判断,分别在判断r
是否不等于size
和w
是否不等于size
。这里面的r
和w
之前都说过,是数组的索引,那么他们具体是什么的索引呢?其实他们都是目前操作数组elementData
的索引。只不过,r
代表操作之前的数组,而w
则代表操作之后的数组。r
主抓遍历,w
主抓存储。所以这里面的r
和w
也可以理解为(就是)read
和write
两个字母的简写。
1. 那么我们先看第一个判断if (r != size)
。这个判断诡异的很,因为按照我们正常的流程来看,上面for循环的代码操作完,这个r
绝对是会等于size
的,因为你遍历了整个数组。那么什么时候这个r
不等于size
呢?只有两种情况:
1.1. r < size
:没有遍历完整个数组,也就是出现异常了;
1.2. r > size
:怎么会有这种情况呢?因为并发操作。要记住,我们ArrayList
可不是线程安全的。当然,这种情况不需要考虑,为什么呢?因为System.arraycopy(elementData, r, elementData, w, size - r);
这个方法会对我们的索引进行检查,如果超出了数组范围就会抛出IndexOutOfBoundsException
异常。
那么出现了上述第一种的问题以后怎么办呢?这就是if判断成立后的代码了。我们先把结论说出来,这里需要保留“犯罪现场”,之前处理过的无所谓了,但是未处理的一定要保留下来。仔细看,这里是一个数组复制。
针对哪个数组呢?针对的就是我们现在操作的数组elementData
,源数组和目标数组都是它。
从哪里复制呢?从“案发现场”复制,也就是此时的r
开始,因为你是在r
这里出现的问题。
复制多少个呢?当然是从r
开始,一直到该数组elementData
最后一个元素结束,也就是size - r
个。
为什么是size - r
个呢?因为你就是从r
这里出现的问题,r
索引之前的数据无论删除操作还是保留操作,你都遍历到这里了,这说明r
以前的数据已然是做过处理了(要么进行了存储,要么进行了跳过),我们无需担心了,需要担心的则是r
索引之后的数据——size - r
个元素。
复制到哪里呢?复制到我们操作的数组elementData
理论上应该执行下一刻存储的地方——w
处。因为操作后的数组elementData
我们中最后需要的“结果数组”,而这个“结果数组”是由w
索引控制的。这个w
的存在就是我们的for循环下一次循环判断后需要存储符合判断条件的元素位置。
这个时候你就会发现,这里其实是做了一个数组拼接。即,将未遍历的元素与现有操作后的元素拼到了一起。操作完这一步后,我们还需更改w
的值,也就是更改现在w
的索引位置。
那么w
索引现在的位置在哪里呢?在w + (size - r)
这个地方。其中的(size - r)
是未遍历的元素个数,w
则是当前操作后的数组elementData
的元素个数(不要忘了w
虽说是索引,但是到这一步它已经完成了w++
的操作,这时的w
已经是之前元素索引的下一个位置了,也就是可以表示当前操作后的数组elementData
的元素个数了)。那么这两者一相加,就得到了最后这个复制完数组的元素个数,即已完成的+未完成的。最后,将这个得数还赋予w
,这也就是代码中所写到的w += size - r
。
好,到这里,我们就解析完了第一组判断的代码。不过这里有一个提问,只要好好看了我这篇博客讲的东西,这个提问其实都不必在这里写。那就是数组是复制完毕了,那么就一定保证我们的数组个数完整不变吗?情况好的话,能拼接出之前大小的数组,这相当于这个数组elementData
就没变;情况不好的话就会出现数组的缩减,缩减就缩减,那么那些多余出来的元素呢?理应删除吧?
下面我们来讲这个所谓“理应删除”的操作。
2. 接下来看第二个判断if (w != size)
。这个判断是在问w
是否不等于size
,那么我们说,什么时候这个w
等于了size
呢?那就是,无论我们是删除操作还是保留操作,这个数组就根本没动,所以,这个时候w
是等于size
的。你想,你要删除或保留一部分东西,这个操作后的数组elementData
一定会是缩小的。相反数组没有缩小要么就是删除操作一个都没删除,要么就是保留操作全部被保留。这不就是数组没动吗?所以,一旦w
等于size
了,我们对理论缩容后的数组中多余元素的处理就没必要了。这个时候if判断不成立,直接返回结果false就是合理的了。
那么我们来看那种正常的情况,也就是我们逾期的情况,即w
不等于size
的情况。那么什么时候这个w
不等于size呢?还是只有两种情况:
2.1. w < size
:预期上正常删除或预期上正常保留后的缩容数组;
2.2. w > size
:并发操作,当然这里我们就不解释了,之前的r > size
已经做了充足的说明。
那么出现了上述第一种的问题以后怎么办呢?来,开始“前后呼应”,我们,理应删除那些多余出来的元素。
到了这一步,无论是否出现异常,是中途中断进行了“犯罪现场”保留,还是正常程序进行了正常缩容,到了这里我们都需要将后续多余出来的数组元素进行清空。我们单看这段代码内for循环的elementData[i] = null
就已经证实了我们的想法。它在将一部分元素进行置null
操作,也就是我们正在删除这个操作后的数组elementData
的多余元素。
那么从什么地方开始呢?for循环已经告诉我们了,从w
索引位置开始。因为还是那句话,无论是中途中断进行了“犯罪现场”保留,还是正常程序进行了正常缩容,到了这里,这个w
索引就是我们操作过后的数组elementData
的最后一个元素的后一位(因为这一步的w
是经过w++
得到的)。所以从这里的w
往后,一切都不要。
那么这之后,我们还需修改我们的操作数modCount
,我们操作了多少次呢?其实是在问我们到底清空了多少个元素,也就是说我们需要拿之前的操作数modCount
与被删除了的size - w
个元素相加,即,modCount + (size - w)
。最后,将这个得数还赋予w
,这也就是代码中所写到的modCount += size - w
。
然后,更新当前数组elementData
的元素个数,即size = w
,因为你也就剩w
个了。
再然后,将modified
置为true
,表示我们的程序正确执行,也就是说,此列表因调用而更改,则为真。
最后,将我们的这个modified
返回即可。
到此,我们整个batchRemove(Collection<?> c, boolean complement)
方法就解析完毕了。
同时,我们ArrayList
的增删改查方法到此全部解析完毕(ಥ_ಥ)。
又同时,我们即将开启ArrayList
的遍历功能解析(>ω<)。