欢迎加入Java交流群 512878347 ,欢迎关注微信公众号 以文在线 。
ArrayList是经常使用的容器,它的底层是采用动态数组实现的,它的容量可以自动增长。ArrayList是一个泛型类,它继承了AbstractList类,实现了List、RandomAccess、Cloneable、Serializable接口,如图1所示。本文从ArrayList的部分源代码入手,详细的讲解了ArrayList的存储结构、改取元素、增删元素等知识。
图1 ArrayList的父型层次
存储结构
顺序表(sequence list)是线性表的一种,它是在内存中开辟一段连续的存储空间存放要存入的元素,它把逻辑上相邻的元素存储在物理位置上相邻的存储单元中(存储单元也称为节点,每一个节点存放一个元素),元素之间的逻辑关系由节点的邻接关系来体现。
数组使用的存储结构就是顺序表。例如,要在数组中存储a1、a2、…、ai-1、ai、ai+1、…、an-1、an。由于需要存储n个元素,就需要n节点,这n个节点在内存中必须连续,并且要将逻辑上相邻的元素存储在物理位置上相邻的节点中,比如在逻辑上ai+1是ai的下一元素,那紧邻ai节点(将存储元素ai的节点称为ai节点)的下一个节点就应该存放元素ai+1。如图2所示。
图2 数组的存储结构
在图2所示的存储结构中,节点中只需要存储元素(在链表中,节点中需要存储节点间的逻辑关系),并且要存储多少个元素就使用多少个节点,这样存储空间的利用率就非常高。但是在该存储结构中插入或者删除元素时,由于元素的个数发生了变化,那存储这些元素的节点个数也随之发生变化,就需要在内存中重新开辟存储空间,重新存放元素,增删元素的效率非常低。为了提高顺序表插入和删除元素的效率,顺序表中节点的数量可以大于等于要存储的元素数量。
ArrayList就是这样来存储元素的,它内部维护的是一个Object类型的数组elementData,它本质上就是使用elementData来存储元素的,参见程序1中第10句代码。elementData数组的长度定义为ArrayList的容量,用capacity来表示。ArrayList存储的元素个数size小于等于它的容量capacity,即顺序表中节点的数量大于等于要存储的元素的数量。如图3所示,ArrayList中利用n+N(N为自然数)个节点存储了n个元素,第1个节点到第n各节点依次存储了a1、a2、…、ai-1、ai、ai+1、…、an-1、an,第n+1个节点到第n+N个节点没有存储元素。没有存储元素的节点预留下来增加新元素。
图3 ArrayList的存储结构
变量
程序1给出了ArrayList中的成员变量并对这些成员变量进行了说明。其中:DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA都是空数组,前者仅用于在无参构造器中给变量elementData赋值,其它的情况要使用空数组,就是用后者。MAX_ARRAY_SIZE表示数组的最大长度,由于少部分虚拟机会在数组中存储头部信息(比如数组的长度),头部信息会占用一定的内存,将数组的最大长度设置为Integer.MAX_VALUE - 8是最安全的。如果实在需要,可以将数组的最大长度设置为Integer.MAX_VALUE,这时那些在数组中存储头部信息的虚拟机会抛出运行时异常。
1 // 序列化ID
2 private static final long serialVersionUID = 8683452581122892189L;
3 // 默认初始容量。
4 private static final int DEFAULT_CAPACITY = 10;
5 // 空数组。
6 private static final Object[] EMPTY_ELEMENTDATA = {};
7 // 默认容量的空数组。
8 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
9 // 用于存储元素的数组,ArrayList的容量是该数组的长度。
10 transient Object[] elementData;
11 // 元素的个数。
12 private int size;
13 // 数组的最大长度。
14 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
程序1 ArrayLis的成员变量
程序2给出了ArrayList的父类AbstractList中的成员变量,在该类中只有一个成员变量modCount,它表示从结构上修改列表的次数,结构上修改指的是改变列表中元素的个数,不包含修改元素的值。
1 // 从结构上修改列表的次数。
2 protected transient int modCount = 0;
程序2 AbstractList的成员变量
构造方法
ArrayList类提供了三个构造方法:
1、ArrayList(int initialCapacity):构造一个指定容量的空列表(没有存放元素的列表),参见程序3。
1 /**
2 * 构造一个指定容量的空列表。
3 *
4 * @param initialCapacity 容量
5 * @return IllegalArgumentException 如果指定的容量为负数
6 */
7 public ArrayList(int initialCapacity) {
8 if (initialCapacity > 0) {
9 this.elementData = new Object[initialCapacity];
10 } else if (initialCapacity == 0) {
11 this.elementData = EMPTY_ELEMENTDATA;
12 } else {
13 throw new IllegalArgumentException("Illegal Capacity: "+
14 initialCapacity);
15 }
16 }
程序3 ArrayList构建指定容量的构造器
2、ArrayList():使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA构造一个空列表,参见程序4。
1 /**
2 * 使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA构建一个空列表。
3 */
4 public ArrayList() {
5 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
6 }
程序4 ArrayList的默认构造器
3、ArrayList(Collection<? extends E> c):构造一个包含给定集合所有元素的列表,并且这些元素在列表中存储的顺序和集合的迭代器返回它们的顺序是一致的,参见程序5。其中第11句代码,c.toArray()返回的实际类型(对象类型)可能不是Object数组,这是因为Collection的某些实现类(比如:Arrays$ArrayList),是使用泛型数组来存储元素的,这些类的toArray()方法的返回值保留了实际的类型。
1 /**
2 * 构造一个包含给定集合所有元素的列表,
3 * 并且这些元素在列表中存储的顺序和集合的迭代器返回它们的顺序是一致的。
4 *
5 * @param c 给定集合
6 * @throws NullPointerException
7 * 如果给定的集合为空(将null作为参数传递进来)
8 */
9 public ArrayList(Collection<? extends E> c) {
10 // 将集合转化为数组并赋给elementData。
11 elementData = c.toArray();
12 if ((size = elementData.length) != 0) { //elementData的长度不为0。
13 /*
14 * elementData实际的类型不是Object数组的时候,
15 * 必须将elementData实际的类型变为Object数组。
16 */
17 if (elementData.getClass() != Object[].class)
18 elementData = Arrays.copyOf(elementData, size,
19 Object[].class);
20 } else { //elementData的长度为0。
21 /*
22 * 使用EMPTY_ELEMENTDATA取代集合转换成的数组,
23 * 即将elementData重新赋值为EMPTY_ELEMENTDATA。
24 * 这样做有两个原因:
25 * 1、 c.toArray得到空数组的实际类型可能不是Object数组。
26 * 将elementData重新赋值为EMPTY_ELEMENTDATA,
27 * 可以将elementData的实际类型变为Object数组。
28 * 2、 减少空数组的个数,节约内存空间。
29 * 将elementData重新赋值为EMPTY_ELEMENTDATA,
30 * c.toArray得到的数组会被GC清除。
31 */
32 this.elementData = EMPTY_ELEMENTDATA;
33 }
34 }
程序5 ArrayList构建包含给定集合所有元素的构造器
取改元素
如图3所示,在ArrayList存储结构中,假设第一个节点的地址(首地址)为Loc(a1),每一个节点占用的内存为len,由于顺序表使用连续的存储空间来存放元素,这样可以计算出第i个节点的存储地址,如式(1)所示:
Loc(ai)=Loc(a1)+(i-1)*len (1)
通过式(1)得到的存储地址可以找到第i个节点,然后就可以对该节点进行操作,比如,获取该节点中的元素,修改该节点中的元素等。
ArrayList中的get(int index)方法就是用来获取节点中的元素,即返回列表中位置(索引)为给定位置的元素,参见程序6。
1 /**
2 * 返回列表中索引为给定索引的元素
3 *
4 * @param 给定索引
5 * @return 列表中索引为给定索引的元素
6 * @throws IndexOutOfBoundsException
7 * 如果索引小于0或者大于等于size
8 */
9 public E get(int index) {
10 // 检查索引是否越界。
11 rangeCheck(index);
12 // 返回给定索引的元素。
13 return elementData(index);
14 }
程序6 ArrayList的get(int index)方法
ArrayList中的set(int index, E element)方法就是用来修改节点中的元素,即用给定元素替换列表中索引为给定索引的元素,参见程序7。
1 /**
2 * 用给定元素替换列表中索引为给定索引的元素。
3 *
4 * @param index 给定索引
5 * @param element 给定元素
6 * @return 被替换的元素
7 * @throws IndexOutOfBoundsException
8 * 如果索引小于0或者大于等于size
9 */
10 public E set(int index, E element) {
11 // 检查索引是否越界。
12 rangeCheck(index);
13 // 存储被替换的元素。
14 E oldValue = elementData(index);
15 // 替换元素。
16 elementData[index] = element;
17 // 返回被替换的元素。
18 return oldValue;
19 }
程序7 ArrayList的set(int index, E element)方法
get(int index)方法和set(int index, E element)方法都调用了rangeCheck(index)方法和elementData(index)方法。elementData(int index)方法是以泛型的形式返回elementData数组中索引(下标)为给定索引的元素,参见程序8;rangeCheck(int index)方法是用来检查下标是否越界,参见程序9,该方法调用了outOfBoundsMsg(index)方法,outOfBoundsMsg(int index)方法用来生成越界信息,参见程序10。
1 /**
2 * 以泛型的形式返回elementData数组中下标为给定下标的元素。
3 */
4 E elementData(int index) {
5 return (E) elementData[index];
6 }
程序8 ArrayList的elementData(int index)方法
1 /**
2 * 检查下标是否越界。
3 * 该方法没有检查下标为负数的情况,
4 * 当下标为负数时,一定会通过该下标访问数组,
5 * 通过该下标访问数组会抛出数组下标越界异常,
6 * 数组下标越界异常也是下标越界异常。
7 *
8 * @param 下标
9 */
10 private void rangeCheck(int index) {
11 if(index >= size) {
12 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
13 }
14 }
程序9 ArrayList的rangeCheck(int index)方法
1 /**
2 * 生成越界信息。
3 *
4 * @param index 下标
5 * @return 越界信息
6 */
7 private String outOfBoundsMsg(int index) {
8 return "Index: "+index+", Size: "+size;
9 }
程序10 ArrayList的outOfBoundsMsg(int index)方法
增删元素
如图4所示,要向一个容量capacity(elementData.length)为n+N,元素个数size为n(这n个元素依次是:a1、…、ai-1、ai、…、an)的ArrayList中第i个节点处插入M个元素(这M个元素依次是b1、…、bM)。这时至少需要n+M个节点来存储全部元素(原有的n个元素和将要插入的M个元素),将n+M称为最小容量,用minCapacity来表示。向ArrayList中插入元素分为了5步:
图4 向ArrayList中插入元素
1、判断ArrayList是否是通过无参构造器初始化的并且从来没有进行扩容,即判断elementData是否等于DEFAULTCAPACITY_EM PTY_ELEMENT。如果是的话,就将minCapacity设置为minCapacity和DEFAULT_CAPACITY(参见程序1中第 4句代码)中较大的值。这样能够保证通过无参构造器创建的ArrayList对象扩容后的最小容量为DEFAULT_CAPACITY。参见程序11。
1 /**
2 * 确保内部容量。
3 *
4 * @param minCapacity 最小容量
5 */
6 private void ensureCapacityInternal(int minCapacity) {
7 if(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
8 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
9 }
10 // 调用ensureExplicitCapacity(minCapacity)方法确保列表的容量够用。
11 ensureExplicitCapacity(minCapacity);
12 }
程序11 ArrayList的ensureCapacityInternal(int minCapacity)方法
2、判断是否需要扩容,即判断式(2)是否成立。如果式(2)成立,就需要扩容,进入第3步;如果式(2)不成立,就不需要扩容,直接进入第4步。参见程序12。
1 /**
2 * 确保清晰的容量。
3 *
4 * @param minCapacity 最小容量
5 */
6 private void ensureExplicitCapacity(int minCapacity) {
7 // 对modCount自增。
8 modCount++;
9 if(minCapacity - elementData.length > 0) {
10 grow(minCapacity);
11 }
12 }
程序12 ArrayList的ensureExplicitCapacity(int minCapacity)方法
需要注意的是,式(2)不能写成式(3),即程序12里面第 9句代码中if语句的逻辑表达式不能写成minCapacity > elementData.length,因为前者考虑了溢出问题,后者没有考虑溢出问题。二者的差别通过一个例子说明,比如:minCapacity为Integer.MIN_VALUE,capacity为Integer.MAX_VALUE。在计算机中使用二进制补码存储数值信息,那Integer.MIN_VALUE - Integer.MAX_VALUE等于1,故式(2)是成立的,而式(3)是不成立的。
minCapacity – elementData.length > 0 (2)
minCapacity > elementData.length (3)
3、增加ArrayList的容量(扩容),参见程序13。先将ArrayList的容量扩大1.5倍,得到新的容量newCapacity,然后判断newCapacity - minCapacity < 0(不能写成newCapacity < minCapacity)是否成立。如果成立就将newCapacity设置为minCapacity,图4演示就是这种情况。然后创建一个长度为newCapacity的数组,将elementData指向的数组中全部元素依次存储到新数组的前n个元素中,并将该新数组作为ArrayList内部维护的数组,即将elementData指向新数组。
1 /**
2 * 扩容,以确保该数组至少能够存储minCapacity个元素。
3 * @param minCapacity 最小容量
4 */
5 private void grow(int minCapacity) {
6 // 旧容量。
7 int oldCapacity = elementData.length;
8 /*
9 * 新容量,先扩容一半,即将容量变为原来的1.5倍。
10 * >>是移位运算,oldCapacity >> 1 等价于 oldCapacity / 2,
11 * 前者效率高于后者。
12 */
13 int newCapacity = oldCapacity + (oldCapacity >> 2);
14 // 如果扩容1.5后的新容量小于最小容量,这时将新容量设定为最小容量。
15 if(newCapacity - minCapacity < 0) {
16 newCapacity = minCapacity;
17 }
18 // 如果新容量大于MAX_ARRAY_SIZE,调用hugeCapacity(minCapacity)方法。
19 if(newCapacity - MAX_ARRAY_SIZE > 0) {
20 newCapacity = hugeCapacity(minCapacity);
21 }
22 // 创建新数组并复制元素。
23 elementData = Arrays.copyOf(elementData, newCapacity);
24 }
程序13 ArrayList的grow(int minCapacity)方法
要注意的是,在扩容时得到的新容量newCapacity不能大于数组所允许的最大长度,这是通过调用hugeCapacity(int minCapacity)方法来保证的。hugeCapacity(int minCapacity)方法的作用是分情况获取数组的最大长度,参见程序14。这里minCapacity超过Integer.MAX_VALUE后一定得到负数,这是因为minCapacity不会超出Integer.MAX_VALUE很多,最多超出Integer.MAX_VALUE一倍,故minCapacity不可能超出Integer.MAX_VALUE后又回到正数。
1 /**
2 * 分情况获取数组允许的最大长度。
3 * @param minCapacity 最小容量
4 * @return 数组允许的最大长度
5 */
6 private static int hugeCapacity(int minCapacity) {
7 // 最小容量大于数组所允许的最大长度,则抛出内存溢出错误。
8 if(minCapacity < 0) {
9 throw new OutOfMemoryError();
10 }
11 /*
12 * 最小容量大于MAX_ARRAY_SIZE,
13 * 返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE。
14 */
15 return (minCapacity > MAX_ARRAY_SIZE) ?
16 Integer.MAX_VALUE : MAX_ARRAY_SIZE;
17 }
程序14 ArrayList的hugeCapacity(int minCapacity)方法
4、移动旧元素,即将即将elementData数组中下表为i-1的元素(第i个节点中的元素)及其以后的元素依次向后移动M个位置,即将元素ai、ai+1、…、an依次存储到第i+M个节点、第i+1+M个节点、…、第n+M个节点中。
5、插入新元素,即将要插入的M个元素b1、…、bM依次插入到第i个节点、…、第i-1+M个节点中。
ArrayList类提供了四个增加元素的方法:
1、add(E e):追加给定元素到列表的末尾,这时不需要移动旧元素,即不需要上述步骤的第4步,参见程序15。
1 /**
2 * 追加给定元素到列表的末尾。
3 *
4 * @param e 追加的元素。
5 * @return <tt><true</tt> (被{@link Collection#add}规定)
6 */
7 public boolean add(E e) {
8 /*
9 * 确保列表有足够的容量来存放要追加的元素。
10 * 要在列表中追加一个元素,
11 * 数组elementData所需要的最小容量为size+1。
12 */
13 ensureCapacityInternal(size + 1);
14 // 将e追加到列表的末尾并对列表元素的个数进行自增。
15 elementData[size++] = e;
16 return true;
17 }
程序15 ArrayList的add(E e)方法
2、addAll(Collection<? extends E> c):将给定集合的所有元素按照集合的迭代器返回的顺序依次追加到列表的末尾,参见程序16。
1 /**
2 * 将给定集合的所有元素按照集合的迭代器返回的顺序依次追加到列表的末尾。
3 * 执行该方法的时候不允许对给定集合发生修改。
4 * 如果当前列表不为空集,不能将它自身作为该方法的参数。
5 *
6 * @param c 给定集合
7 * @return <tt>true</tt> 如果当前列表发生改变
8 * @throws IndexOutOfBoundsException
9 * 如果下标小于0或者大于size
10 * @throws NullPointerException 如果给定集合为空
11 */
12 public boolean addAll(Collection<? extends E> c) {
13 // 将给定集合转换为数组
14 Object[] a = c.toArray();
15 // 获取a的长度(给定集合的元素个数)。
16 int numNew = a.length;
17 /*
18 * 确保列表有足够的容量来存放要追加的元素。
19 * 要在列表中追加numNew个元素,
20 * 数组elementData所需要的最小容量为size+numNew。
21 */
22 ensureCapacityInternal(size + numNew);
23 // 将数组a的所有元素追加到elementData数组中。
24 System.arraycopy(a, 0, elementData, size, numNew);
25 // 修改列表中元素的个数。
26 size += numNew;
27 // numNew != 0 代表集合发生了改变。
28 return numNew != 0;
29 }
程序16 addAll(Collection<? extends E> c)方法
3、add(int index, E element):在列表中索引为给定索引处插入给定元素,参见程序17。
1 /**
2 * 在列表中索引为给定索引处插入给定元素。
3 * 移动列表中索引为给定索引的元素(如果有的话)
4 * 及其以后的元素到它的右边(将它们的下标加1)。
5 *
6 * @param index 给定索引。
7 * @param element 要插入的元素。
8 * @throws IndexOutOfBoundsException
9 * 如果下标小于0或者大于size
10 */
11 public void add(int index, E element) {
12 //检查索引是否越界。
13 rangeCheckForAdd(index);
14 // 确保列表有足够的容量来存放要插入的元素。
15 ensureCapacityInternal(size + 1);
16 // 将列表中索引为给定索引的元素及其以后的元素依次向后移动一个位置。
17 System.arraycopy(elementData, index, elementData,
18 index+1, size-index);
19 // 将给定元素插入到指定位置。
20 elementData[index] = element;
21 // 对元素的个数进行自增。
22 size++;
23 }
程序17 ArrayList的add(int index, E element)方法
4、addAll(int index, Collection<? extends E> c):将给定集合的所有元素按照集合的迭代器返回的顺序依次从给定索引插入到列表中,参见程序18。
1 /**
2 * 将给定集合的所有元素按照集合的迭代器返回的顺序
3 * 依次从给定索引插入到列表中。
4 * 移动列表中索引为给定索引的元素(如果有的话)
5 * 及其以后的元素到它的右边(增加它们的下标)。
6 、*
7 * @param index 给定集合的第一个元素插入的位置
8 * @param c 给定集合
9 * @return <tt>true</tt> 如果当前列表发生改变
10 * @throws NullPointerException 如果给定集合为空
10 */
11 public boolean addAll(int index, Collection<? extends E> c) {
12 //检查索引是否越界。
13 rangeCheckForAdd(index);
14 Object[] a = c.toArray();
15 int numNew = a.length;
16 // 确保列表有足够的容量来存放要插入的元素。
17 ensureCapacityInternal(size + numNew);
18 // 要移动的元素的个数。
19 int numMoved = size - index;
20 // 如果有要移动的元素,将这些元素依次向后移动numNew个位置。
21 if(numMoved > 0) {
22 System.arraycopy(elementData, index,
23 elementData, index + numNew, numMoved);
24 }
25 // 将数组a的所有元素插入到elementData数组中。
26 System.arraycopy(a, 0, elementData, index, numNew);
27 size += numNew;
28 return numNew != 0;
29 }
程序18 ArrayList的addAll(int index, Collection<? extends E> c)方法
add(int index, E element)方法和addAll(int index, Collection<? extends E> c)方法都调用了rangeCheckForAdd(int index)方法,该方法是专门为add和addAll方法提供的用来检查下标是否越界,参见程序19。该方法也调用了outOfBoundsMsg(index)方法(参见程序10)生成越界信息。
1 /**
2 * 专门为add和addAll方法提供的检查下标是否越界。
3 *
4 * @param index 下标
5 */
6 private void rangeCheckForAdd(int index) {
7 if(index > size || index <0) {
8 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
9 }
10 }
程序19 ArrayList的rangeCheckForAdd(int index)方法
如图5所示,要从一个容量capacity为n+N,元素的个数为n(这n个元素依次是:a1、…、ai-1、ai、ai+1、…、an)的ArrayList中删除与给定元素ai相同的元素分为了2步:
1、在当前列表中查找与给定元素ai相同的元素。如果在当前列表中找到了与给定元素相同的元素,进入第2步;如果在当前列表中没有找到与给定元素相同的元素,就结束操作,这时没有删除任何元素。
2、将与给定元素ai相同的元素从列表中删除,可以通过将要删除元素后面的所有元素依次向前移动一个位置来实现。
图5 从ArrayList中删除元素
ArrayList类提供了三个删除元素的方法:
1、remove(int index):删除列表中索引为给定索引的元素,这时不需要查找元素,即不需要上述步骤的第1步,参见程序20。该方法调用了rangeCheck(index)方法(参见程序9)来检查下标是否越界。
1 /**
2 * 删除列表中索引为给定索引的元素。
3 * 移动列表中索引为给定索引以后的所有元素到它的左边(将它们的下标减去1)。
4 *
5 * @param index 给定索引
6 * @return 被删除的元素
7 * @throws IndexOutOfBoundsException
8 * 如果下标小于0或者大于等于size
9 */
10 public E remove(int index) {
11 // 检查索引是否越界。
12 rangeCheck(index);
13 // 对modCount自增。
14 modCount++;
15 // 获取删除的元素。
16 E oldValue = elementData(index);
17 // 要移动的元素的个数。
18 int numMoved = size - index -1;
19 // 如果有要移动的元素,将这些元素依次向前移动一个位置。
20 if(numMoved > 0)
21 System.arraycopy(elementData, index + 1,
22 elementData, index, numMoved);
23 // 将列表的最后一个元素赋值为null并对列表元素的个数进行自减。
24 elementData[--size] = null;
25 // 返回删除的元素。
26 return oldValue;
27 }
程序20 ArrayList的remove(int index)方法
2、remove(Object o):删除列表中与给定元素相同的第一个元素,如果列表中没有和给定元素相同的元素,就不进行操作,参见程序21。
1 /**
2 * 删除列表中与给定元素相同的第一个元素。
3 * 如果列表中没有和给定元素相同的元素,就不进行操作。
4 *
5 * @param o 给定元素
6 * @return <tt>true</tt> 如果从列表中删除了元素
7 */
8 public boolean remove(Object o) {
9 if( o == null) { // 给定元素为null。
10 // 遍历集合的所有元素。
11 for(int index=0;index<size;index++) {
12 /*
13 * 如果集合的某一个元素为null,
14 * 调用fastRemove(index)删除该元素并返回true。
15 */
16 if(elementData[index] == null) {
17 fastRemove(index);
18 return true;
19 }
20 }
21 } else { //给定元素不为null。
22 // 遍历集合的所有元素。
23 for(int index=0;index<size;index++) {
24 /*
25 * 如果集合的某一个元素和给定元素相同,
26 * 调用fastRemove(index)删除该元素并返回true。
27 */
28 if(o.equals(elementData[index])) {
29 fastRemove(index);
30 return true;
31 }
32 }
33 }
34 // 程序执行到这里,集合中一定不包含给定元素。
35 return false;
36 }
程序21 ArrayList的remove(Object o)方法
在remove(Object o)方法中是调用fastRemove(int index)方法,该方法是专门为remove(Object o)提供的用来快速删除元素,参见程序22。该方法与remove(int index)方法(参见程序20)的区别是:该方法不进行下标越界检查、不返回删除的旧元素。
1 /**
2 * 快速删除元素的方法,不进行下标越界检查,不返回删除的旧元素。
3 */
4 private void fastRemove(int index) {
5 modCount++;
6 int numMoved = size - index -1;
7 if(numMoved > 0)
8 System.arraycopy(elementData, index + 1,
9 elementData, index, size - 1 - index);
10 elementData[--size] = null;
11 }
程序22 ArrayList的fastRemove(int index)方法
3、removeAll(Collection<?> c):删除列表中所有被给定集合包含的元素,参见程序23。
1 /**
2 * 删除列表中所有被给定集合包含的元素。
3 *
4 * @param c 给定集合
5 * @return {@code true} 如果当前列表发生改变
6 * @throws ClassCastException
7 * 如果当前列表中元素的类型和给定集合元素的类型不匹配
8 * @throws NullPointException
9 * 如果当前列表中包含null给定集合不允许包含null
10 * @see Collection#contains(Object)
11 */
12 public boolean removeAll(Collection<?> c) {
13 // 判断c是否为null,如果c为空会抛出空指针异常。
14 Objects.requireNonNull(c);
15 // 调用batchRemove(c, false)方法删除被给定集合包含的元素。
16 return batchRemove(c, false);
17 }
程序23 ArrayList的removeAll(Collection<?> c)方法
在removeAll(Collection<?> c)方法中调用了batchRemove(Collection<?> c,boolean complement)方法,该方法是根据标识(传递的参数complement)删除或保留列表中被给定集合包含的元素,参见程序24。如果complement为false,保留没有被给定集合包含的元素(删除被给定集合包含的元素);如果complement为true,保留被给定集合包含的元素。
1 /**
2 * 根据给定标识删除或保留列表中被给定集合包含的元素。
3 *
4 * @param c 给定集合
5 * @param complement 给定标识
6 * @return {@code true} 如果当前列表发生改变
7 */
8 private boolean batchRemove(Collection<?> c,boolean complement) {
9 // 获取集合内部维护的数组。
10 final Object[] elementData = this.elementData;
11 // 数组下标变量r,列表新的元素个数w。
12 int r = 0, w = 0;
13 // 定义变量记录列表是否发生改变。
14 boolean modified = false;
15 try {
16 for(;r<size;r++)
17 /*
18 * 如果complement为false,
19 * 保留没有被给定集合包含的元素(删除被给定集合包含的元素);
20 * 如果complement为true,
21 * 保留被给定集合包含的元素。
22 *
23 * c.contains可能会抛出类型转换异常或空指针异常。
24 */
25 if(c.contains(elementData[r]) == complement)
26 elementData[w++] = elementData[r];
27 } finally {
28 // r!=size 表示执行try里面的c.contains()抛出了异常,
29 if(r != size) {
30 // 将抛出异常处的那一个元素及其以后的元素保存下来。
31 System.arraycopy(elementData, r, elementData, w, size - r);
32 w += size -r;
33 }
34 // w!=size 表示列表发生了改变。
35 if(w != size) {
36 // 将下标为w到size-1的元素赋值为null。
37 for(int i =w;i<size;i++)
38 elementData[i] = null;
39 /*
40 * 修改modCount,
41 * 删除的元素个数就是就是从结构上修改列表的次数。
42 */
43 modCount += size - w;
44 size = w;
45 modified = true;
46 }
47 }
48 return modified;
49 }
程序24 ArrayList的batchRemove(Collection<?> c,boolean complement)方法
总结
1、ArrayList不是线程安全的,它允许元素的值为null,使用时需要注意。
2、如果事先可以预知数据量的大小,可以通过public ArrayList(int initialCapacity)构造方法来指定集合的大小,以减少扩容次数,提高写入效率。
2、取改元素通过数组下标进行,时间复杂度相对较低;增删元素可能会导致数组扩容和数组复制,时间复杂度相对较高。故在数据量比较大的情况下,取改元素使用较多,增删元素使用较少,适合使用ArrayList。
局部内部类是在方法中定义的类。它的可见范围是当前方法,和局部变量一样,局部内部类不能用访问控制修饰符(public、private以及protected)和静态修饰符static来修饰。局部内部类中除了可以访问外部类的所有成员,还可以访问所在方法的最终变量或参数(被final修饰的变量或参数),从JDK8.0开始,还可以访问所在方法的实际上的最终变量或参数(没有被final修饰但只进行了一次赋值的变量或参数)。为什么局部内部类中只能访问所在方法的最终变量或参数以及实际上的最终变量或参数呢?
先来验证局部内部类中只能访问所在方法的最终变量或参数以及实际上的最终变量或参数。如例1_1所示,在外部类Out_1的method方法中有一个参数a,两个变量b、c,并在局部内部类Inner_1中访问了这三个量。由于参数a是被final修饰的,那参数a就是最终的参数,故在Inner_1中可以访问它,第7句代码没有编译错误;虽然变量b没有被final修饰,但是变量b仅仅被赋值一次,它是实际上的最终变量,故在Inner_1中可以访问它,第8句代码没有编译错误;变量c没有被final修饰并且被赋值了两次,它不是最终变量也不是实际上的最终变量,故在Inner_1中不可以访问它,第9句代码就有了编译错误。可以得到,在局部内部类中只能访问所在方法的最终变量或参数以及实际上的最终变量或参数。
1 class Out_1 {
2 public void method(final int a) {
3 final int b = 1;
4 int c = 2;
5 c= 3;
6 class Inner_1 { 7 int d = a; //编译正确,访问final修饰的参数。 8 int e = b; //编译正确,访问实际上的最终变量。 9 int f = c; //编译错误,c既不是最终变量 10 //也不是实际上的最终变量。 11 } 12 } 13 }
例1_1 Out_1.java
接着再来讨论为什么局部内部类中只能访问所在方法的最终变量或实际上的最终变量。这里只讨论方法的变量,对于方法的参数它的道理是一模一样的,因为方法的参数本质上就是方法的变量。要知道局部内部类和外部类是处于同一级别的,局部内部类不会因为定义在方法中就随着方法的执行完毕而销毁。如例1_2所示,在外部类Out_2的method1方法中定义了一个局部内部类Inner_2,然后创建了一个线程t并启动t线程,然后method1方法就执行结束,局部变量m就被销毁。线程t启动之后,会先睡眠1000毫秒,然后创建了局部内部类Inner_2的对象,并通过该对象去调用了method2方法,在method2方法中访问了method1方法定义的变量m,由于method1方法早已执行结束,变量m已经消失。这样就出现了一个矛盾:内部类对象访问了一个不存在的变量。
1 class Out_2 {
2 public void method1() {
3 int m = 4;
4 class Inner_2 {
5 public void method2() {
6 System.out.println(m); 7 } 8 } 9 new Thread("t") { 10 public void run() { 11 try { 12 Thread.sleep(1000); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 new Inner_2().method2(); 17 } 18 }.start(); 19 } 20 }
例1_2 Out_2.java
为了解决这个矛盾,如果局部内部类中访问了所在方法的某个变量,就将该方法中的变量复制一份作为内部类的成员变量,当内部类访问所在方法中的变量时,就让它去访问复制出来的成员变量。这样在内部类所在方法中的变量消失的之后,仍然可以访问它,当然这里并不是真正地访问它,而是访问它的复制品。这里需要注意,由于是将局部内部类所在方法的变量复制一份作为局部内部类的成员变量,故在定义局部内部类之前,一定要对局部内部类所在方法的变量进行初始化,没有初始化是无法复制的。在例1_3所示的代码中,第6句代码是有编译错误的。
1 class Out_3 {
2 public void method1() {
3 int m;
4 class Inner_3 {
5 public void method2() {
6 m = 4; //编译错误,m应在定义内部类
7 //之前进行初始化。
8 System.out.println(m); 9 } 10 } 11 } 12 }
例1_3 Out_3.java
但是这样做又有一个问题,那就是必须时时刻刻保证复制得到的那一份成员变量的值和原来的局部变量的值相同。如果在外部类中修改了局部变量的值,那就要修改局部内部类中复制得到的那一份成员变量的值;如果在局部内部类中修改了复制得到的那一份成员变量的值,那就要修改外部类中局部变量的值(前提是这个局部变量还存在),这样做是非常困难的。于是Java干脆就不允许局部内部类要访问的局部变量的值发生改变,即局部内部类中只能访问所在方法的最终变量或实际上的最终变量。