1. ArrayList概述
- 回忆一下,遇到元素个数不确定时,就会使用ArrayList,而非数组
1.1 ArrayList的特性
- 同样的,还是通过阅读类注释了解ArrayList的特性
ArrayList的类注释,提供了以下信息
-
ArrayList实现了List接口,允许元素为
null
-
ArrayList是动态的list实现,提供了用于操作数组大小的方法
- 容量,指ArrayList中存储元素的数组大小;size,指ArrayList中的元素个数
- 向ArrayList中添加元素,其容量会自动增长(扩容)。为避免添加大量元素触发ArrayList多次扩容的情况,可以使用
ensureCapacity()
方法将其容量指定为一个合适的较大值 - 注意:
trimToSize()
方法,可以将ArrayList的容量设置为当前size,使其不存在存储空间冗余
-
ArrayList支持随机访问,get和set操作时间复杂为 O ( 1 ) O(1) O(1);add操作大部分情况下时间复杂度为 O ( 1 ) O(1) O(1),除了触发扩容或从中间插入的情况
-
ArrayList使用fail-fast迭代器:
- 一旦通过
iterator()
或listIterator()
创建迭代器,除非使用迭代器自身的remove或add方法,任何修改list结构的方法都将使迭代器抛出ConcurrentModificationException
异常 - 注意: 所谓的add方法,是
listIterator()
创建ListIterator迭代器所有特有的方法;而remove方法,是iterator()
创建的Iterator迭代器中的方法ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { if (iterator.next().equals(2)) { iterator.remove(); } } ListIterator<Integer> listIterator = list.listIterator(); while (iterator.hasNext()) { if (listIterator.next().equals(1)) { listIterator.add(21); } }
- 一旦通过
-
ArrayList与Vector是等价的,除了ArrayList是非线程安全的之外。
- 多线程访问ArrayList,最好使用
Collections.synchronizedList()
方法将其转为线程安全的list - 或者使用线程安全的Vector类(使用
synchronized
实现线程安全), - 或者使用concurrent包下、读写分离的
CopyOnWriteArrayList
- 多线程访问ArrayList,最好使用
总结
- ArrayList实现了List接口,允许
null
值 - ArrayList基于动态数组,支持随机访问;插入元素会自动扩容,也可以使用
ensureCapacity()
方法方法手动扩容 - ArrayList是非线程安全的,Vector是线程安全的
- ArrayList使用fail-fast迭代器(两种迭代器、remove和add方法)
1.2 ArrayList类图
-
ArrayList类的声明如下
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
-
类图如下
- 直观地看,ArrayList继承了抽象类AbstractList,实现了List、RandomAccess、Cloneable和Serializable接口
- 直观地看,ArrayList继承了抽象类AbstractList,实现了List、RandomAccess、Cloneable和Serializable接口
-
AbstractList
抽象类:- 对List接口的骨架级实现,可以最小化实现一个支持随机访问的数据结构(如,数组)所需的工作量;
- 与之对应的,
AbstractSequentialLis
抽象类,最小化实现一个支持顺序访问的数据结构(如,链表)所需的工作量。
-
List
接口:不允许重复元素的集合,允许null
元素 -
RandomAccess
接口:- 实现List时的标记接口,以表明该list支持快速的随机访问。
- 当应用于随机或顺序访问的list时,该接口的主要作用是允许一般的算法修改其行为以提供良好的性能
- 说实话,自己不太理解,可能需要深入了解标记接口的作用 😂
-
Cloneable
接口:表明ArrayList支持拷贝,其具体实现为浅拷贝 -
Serializable
接口:表明ArrayList支持拷贝支持序列化
1.3 成员变量
-
ArrayList的类常量如下
// 默认初始容量为10 private static final int DEFAULT_CAPACITY = 10; // 一个共享的空数组实例 // 若用户通过构造函数,指定一个大小为0的ArrayList,将使用EMPTY_ELEMENTDATA初始化elementData private static final Object[] EMPTY_ELEMENTDATA = {}; // 一个共享的空数组实例,用于具有默认容量的ArrayList // 使用DEFAULTCAPACITY前缀与EMPTY_ELEMENTDATA相区别,以便知道在第一次添加元素时如何扩容 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 最大的容量,如果超过该容量容易导致OOM private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
-
实例变量
// 存储元素的数组缓冲区,ArrayList的容量就是数组缓冲区的长度 // 未指定初始化容量的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,在第一次添加元素时,将被扩容为10 transient Object[] elementData; // non-private to simplify nested class access // ArrayList中的元素个数,size <= capacity private int size;
-
存储元素的数组缓冲区,使用
transient
关键字修饰,即该成员变量无法序列化和反序列化
1.4 如何实现ArrayList中元素的序列化?
- 存储元素的数组elementData被
transient
修饰,那岂不是就不能序列化和反序列化了? - 别着急,ArrayList提供了
writeObject()
和readObject()
方法,用于实现序列化和反序列化
writeObject()
方法
- 代码如下:
- 除了序列化非static、非transient元素,还特意将size和数组中存储的元素序列化了
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; // 将当前类的非static、非transient字段写入输出流 s.defaultWriteObject(); // 将size作为与clone操作行为上兼容的capacity,实际上已经将size序列化过了 s.writeInt(size); // 将元素顺序写入输出流 for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } // 序列化后,发现结构被修改,则抛出ConcurrentModificationException if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
writeObject()
方法
-
代码如下:
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 // 这里的size是通过 s.defaultReadObject() 得到的 if (size > 0) { // be like clone(), allocate array based upon size not capacity int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
疑问一:两个方法都被定义为private,岂不是无法使用?
-
仔细观察发现,序列化和反序列化的两个方法,都被定义为
private
-
那不就无法通过
list.writeObject()
进行调用了? -
通过debug发现调用
ObjectOutputStream.writeObject(Object obj)
时,实际会调用obj的writeObject() 方法
-
readObject()方法也是如此,调用
ObjectInputStream.readObject()
方法,实际obj的readObject() 方法 -
验证代码示例
public static void main(String[] args) throws ParseException { ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(3); list.add(5); // 序列化list try (FileOutputStream fileOutputStream = new FileOutputStream("list.txt" ); ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream) ) { outputStream.writeObject(list); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException exception) { exception.printStackTrace(); } ArrayList<Integer> list1 = new ArrayList<>(); // 反序列化 try (FileInputStream fileInputStream = new FileInputStream("list.txt"); ObjectInputStream inputStream = new ObjectInputStream(fileInputStream) ) { list1 = (ArrayList<Integer>) inputStream.readObject(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException exception) { exception.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } list1.forEach(System.out::println); }
参考文档
- ArrayList 序列化
- 基于文件的java IO(推荐一波自己的博客,好久没有使用IO了,如何创建输入输出流都忘光了😂)
1.5 构造函数
-
ArrayList具有三个构造函数
// 指定初始化容量的构造函数,如果初始化容量为0,使用共享变量标EMPTY_ELEMENTDATA记此种情况 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); } } // 默认构造函数,使用共享变量标DEFAULTCAPACITY_EMPTY_ELEMENTDATA记此种情况 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 从给定的集合类构造ArrayList 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; } }
-
后续的学习中,需要关注第一次添加元素时,
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和EMPTY_ELEMENTDATA
如何影响扩容的
2. 查找方法
2.1 根据索引查找元素
-
作为一个支持随机访问的动态数组,虽然不能像数组一样,通过
arr[i]
直接访问某个元素 -
但List接口定义的
public E get(int index)
方法,能达到同样的效果 -
ArrayList的get方法代码如下,就两行代码 😂
- 先检查index是否越界,若越界,则直接抛出IndexOutOfBoundsException
- 通过
elementData(index)
返回elementData[index],该方法实现了类型转换
public E get(int index) { rangeCheck(index); // 检查索引是否越界,若越界,抛出IndexOutOfBoundsException return elementData(index); // 等价于 elementData[index] } private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } E elementData(int index) { return (E) elementData[index]; }
疑问:为何单独进行下标越界检查?越界检查的范围,好像不太对?
- 数组下标为 0 ~ len - 1,通过arr[index]访问时,Java在运行时会自动抛出
ArrayIndexOutOfBoundsException
异常 - 这样来看,根本不用单独进行下标越界检查的
- 单独进行下标越界检查的原因:
- 如果依靠数组自身的越界检查,则可能访问到还未存入元素的位置
- size才是实际存储的元素个数,超过size的位置,元素值默认为
null
- 那这样分析的话,rangeCheck 方法应该检查
index < 0 || index >= size
的情况,为何忽略了index < 0
的检查? - 原因:作者偷懒了,想依靠数组自身的越界检查实现
index < 0
的check 😎
2.2 查找元素的索引
-
能根据索引查找元素,就有需求:根据元素查找索引
-
ArrayList允许存在重复的元素,返回的索引应该是第一个匹配元素的位置;或者,需要直接返回最后一个匹配元素的位置
-
对应的方法为
indexOf()
和lastIndexOf()
-
indexOf()
方法的代码如下:- ArrayList允许
null
值,首先处理查找null
值索引的情况 - 一旦查到匹配的元素,则立即返回对应的索引;未找到,则返回-1
public int indexOf(Object o) { if (o == null) { // 首先处理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 }
- ArrayList允许
-
lastIndexOf()
方法的代码如下:- 要是按照我的菜鸟思路:从前往后查找匹配的元素,使用一个变量记录查找到的index,最后记录的就是lastIndex或-1 😂
- too young, too simple啊,看看人家写的:直接从后往前找,第一个匹配元素的索引就是lastIndex
- 同样的,还是按照
null
值和一般值两种情况进行查找;未找到,则返回-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; }
2.3 判断是否包含元素
-
之前,一直都未注意到list有关的类中提供了
contains()
方法,使用contains方法更多的在set和map类中 -
其实,
contains()
方法是List接口、List接口的父接口Collection,均有的一个方法,用于判断集合中是否包含指定的元素 -
contains()
方法的代码如下:只要元素的索引不为-1,即大于等于0,则说明ArrayList中包含该元素public boolean contains(Object o) { return indexOf(o) >= 0; }
3. 添加方法 —— 核心:扩容
3.1 add方法
-
使用ArrayList时,经常出现类似的代码
list.add(1);
,这是最简单的添加方法,会直接在末尾添加一个元素 -
add方法的代码如下,看起来简单,实则暗含玄机😜
- 先通过
ensureCapacityInternal()
方法,根据实际情况决定是否对内部的数组缓冲区进行扩容 - 将元素添加到数组末尾,size++;返回true,表示成功添加元素
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! 由ensureExplicitCapacity方法完成 elementData[size++] = e; return true; }
- 先通过
重点来了,ensureCapacityInternal()
方法是如何实现扩容的?
- ensureCapacityInternal方法的代码如下:从方法名上理解,先计算所需容量,然后按需扩容以保证具有指定的容量
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
- calculateCapacity 方法的代码如下:
- 如果elementData为
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,即未指定容量的空数组,计算出的容量为max(DEFAULT_CAPACITY, minCapavity)
- 否则,计算出的容量就是指定容量minCapacity
- 总结: 这里有体现
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
在第一次添加元素时的作用:决定初始化容量
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
- 如果elementData为
- 计算出所需的容量后,通过 ensureExplicitCapacity 方法保证其具有充足的容量
- 无论最后是否需要扩容,添加元素都会改变ArrayList的结构,因此需要
modCount++
- 若所需容量超过当前容量,则调用
grow()
方法实现扩容
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
- 无论最后是否需要扩容,添加元素都会改变ArrayList的结构,因此需要
3.2 grow()方法实现扩容
-
学习了HashMap,HashMap中初始容量要求为2的幂,扩容也是扩容两倍
- 好处一: 通过
(n - 1) & hash
计算出来的index分布可以更加均匀,减少哈希冲突 - 好处二: 按照2倍扩容,扩容后元素的位置更容易计算(
e.hash & oldCap
):原位index,或index + oldCap
- 好处一: 通过
-
回到ArrayList的扩容,计算出来的
minCapacity
很可能就是size + 1 -
一个坑一个坑的扩容,这也太费力了,肯定不能这样 🤔
-
grow 方法的代码如下
- 首先,根据当前容量oldCapacity初始化newCapacity,
newCapacity = oldCapacity * 1.5
,扩容1.5倍 - 使用指定容量修正newCapacity,兼容第一次初始化的情况
- 若指定容量为size + 1,则不可能出现
newCapacity - minCapacity < 0
的情况 - 通过公有的
ensureCapacity()
方法扩容,或者oldCapacity为0时,才需要使用指定容量修正
- 若指定容量为size + 1,则不可能出现
- 通过
MAX_ARRAY_SIZE
修正newCapacity,避免过大而引发OOMnewCapacity - MAX_ARRAY_SIZE > 0
的原因:(1)扩容1.5倍后,超过MAX_ARRAY_SIZE
;(2)指定容量太大,使得newCapacity超过MAX_ARRAY_SIZE
- 最后,创建一个容量为newCapacity的新数组;原数组中的元素通过
System.arraycopy()
实现拷贝
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 默认为oldCapacity的1.5倍 if (newCapacity - minCapacity < 0) // 使用指定容量更新newCapacity,可以兼容第一次初始化的情况 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) // 修正newCapacity,避免过大而引发oom newCapacity = hugeCapacity(minCapacity); // 创建一个容量为newCapacity的新数组,通过System.arraycopy实现元素的拷贝 elementData = Arrays.copyOf(elementData, newCapacity); }
- 首先,根据当前容量oldCapacity初始化newCapacity,
-
hugeCapacity() 方法的代码如下
- 若指定容量过大,导致newCapacity超过
MAX_ARRAY_SIZE
,则将其设置为Integer.MAX_VALUE
;否则,直接截断设置为MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
- 若指定容量过大,导致newCapacity超过
总结:
- ArrayList的扩容,默认为1.5倍;若指定容量大于1.5倍,则使用指定容量修正
- 为了避免newCapacity过大,还需要使用
MAX_ARRAY_SIZE
进行修正 - 一般情况下,都是按照1.5被扩容的(也有可能使用指定容量进行扩容)
3.3 native方法System.arraycopy()
-
扩容的最后一步操作:调用
Arrays.copyOf()
方法,基于现有数组创建一个指定容量的新数组 -
如果新数组的长度小于原数组,则会发生数据截断
public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); // 拷贝的数据长度为新旧长度的min,数据截断的关键 return copy; }
-
Arrays.copyOf()
方法通过调用native方法System.arraycopy()
实现数组元素的拷贝length
指需要拷贝的数据长度Arrays.copyOf()
方法巧妙使用Math.min(original.length, newLength)
实现数据的截断
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
3.4 指定index的add方法
- List接口还贴心地提供了指定index的add方法,再也不用担心只能尾部插入的问题了
- 注意: 与add(E element)方法不同的是,该方法无返回值;可以通过是否返回
IndexOutOfBoundsException
异常,判断是否成功添加元素
public void add(int index, E element) { rangeCheckForAdd(index); // 检查指定的index是否越界;若越界,则抛出`IndexOutOfBoundsException`异常 ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); // 在index插入元素,则index开始的元素都需要后移,空出对应的位置 elementData[index] = element; size++; } private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
- 注意: 与add(E element)方法不同的是,该方法无返回值;可以通过是否返回
疑问:为何单独定义rangeCheckForAdd() 方法?
- 向ArrayList中添加元素,由于其存在扩容机制,直接向超过
elementData.length
的位置添加按理是允许的 - 但是,实际存储的元素为size个,如果指定的index超过size,那就使得数组中的元素不连续了
- 因此,需要限制
index <= size
;若index == size
,说明刚好在末尾添加元素 - 同时,作者这里没有偷懒了,主动限制
index >= 0
:stuck_out_tongue_winking_eye - 因此,angeCheckForAdd() 方法的check条件为
index > size || index < 0
,就抛出IndexOutOfBoundsException 异常 - 同时,这与获取元素时(get/set 方法)的check方法就存在差异了,从
index >= size
变成了index > size
3.5 多个元素的add
-
有单个元素的add,就有多个元素的add
-
同样,List接口也提供了两种addAll()方法:默认在尾部添加、在指定位置添加
-
这两个方法均具有boolean类型的返回值,true表示成功添加
-
默认在尾部添加多个元素,不再赘述该方法
public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; }
-
在指定位置添加多个元素,不再赘述该方法
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); // 空出numNew个位子 System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; }
4. 修改元素
- 通过get()方法,可以获取指定index的元素;同样的,可以通过set()方法修改元素
- set 方法的代码如下,十分简单:检查index是否越界、记录oldValue、更新value、返回oldValue
public E set(int index, E element) { rangeCheck(index); // check下标是否越界 E oldValue = elementData(index); elementData[index] = element; return oldValue; }
5. 删除元素
- 删除元素,同样支持 “逆向工程”,支持通过index删除元素,或删除第一个匹配的元素
5.1 通过索引删除元素
- 通过index删除元素的remove方法,代码如下
- 应该从已有的元素中删除,因此index应该在 0 ~ index - 1的范围。所以,进行下标越界检查时,调用的是
rangeCheck()
方法,而非rangeCheckForAdd()
方法 - 通过右侧元素整体前移,并将最后一个元素置为
null
,来实现元素的删除 - 该方法将返回被删除元素的值
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; // 断开与元素的连接,以便该元素可以被gc回收 return oldValue; }
- 应该从已有的元素中删除,因此index应该在 0 ~ index - 1的范围。所以,进行下标越界检查时,调用的是
- 删除过程的示意图如下:
5.2 删除指定的元素
-
删除指定元素的remove方法代码如下:
- 与indexOf()方法一样,考虑到ArrayList中可能存在重复元素,采用 “最左匹配原则” ,即删除从头到尾,第一个匹配的元素
- 删除时,根据匹配元素的index进行删除;相比
remove(int index)
方法,多了确定元素索引的步骤 - 该方法不再返回被删除元素或者其index,而是返回
true
或false
,表示是否成功删除元素
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; }
-
其中,
fastRemove(int index)
方法就是remove(int index)
方法的核心代码:右侧元素前移、删除最后一个元素
批量删除的方法:
removeAll(Collection<?> c)
,从ArrayList中删除包含在指定集合中的元素removeIf(Predicate<? super E> filter)
,JDK 1.8的新增方法,删除ArrayList中满足条件的所有元素clear()
,非常暴力的方法,将数组清空,回归到无任何元素的状态
6. for-each与迭代器的关系
- 学习容器类时,或多或少都会提到以下知识:
- 通过for-each遍历,通过迭代器iterator遍历
- 拥有fail-fast迭代器:一旦创建好迭代,除非使用迭代器的remove方法,其他任何修改结构的方法都将触发迭代器抛出ConcurrentModificationEXception 异常
- 当时,学习时并未注意for-each和迭代器的关系:都是遍历,二者是不同的遍历机制;还是本质是同一种遍历机制
- 在集合类的学习中,发现:从JDK 1.5开始,实现了 Iterable 接口的对象,都可以使用for-each进行遍历
- 那就是说,for-each遍历底层是通过迭代器实现的
6.1 for-each的底层实现验证
-
基于ArrayList实现for-each遍历
public static void main(String[] args) throws ParseException { ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); for (Integer e : list) { System.out.print(e + " "); } }
-
在idea中,双击对应的字节码文件,得到反编译后的代码:
public static void main(String[] args) throws ParseException { ArrayList<Integer> list = new ArrayList(); list.add(1); list.add(2); Iterator var2 = list.iterator(); while(var2.hasNext()) { Integer e = (Integer)var2.next(); System.out.print(e + " "); } }
-
结论: for-each遍历底层是通过迭代器实现的
-
若在遍历容器类时试图删除元素的原因:modCount会发生改变,可能会触发迭代器抛出ConcurrentModificationEXception 异常
6.3 ArrayList的两种迭代器
-
ArrayList支持 ListIterator 和 Iterator两种迭代器,分别叫做
ListItr
和Itr
-
创建迭代器的方法为:
public ListIterator<E> listIterator() { return new ListItr(0); } public ListIterator<E> listIterator(int index) { if (index < 0 || index > size) throw new IndexOutOfBoundsException("Index: "+index); return new ListItr(index); } public Iterator<E> iterator() { return new Itr(); }
以Itr为例,在哪里触发fail-fst机制?
-
hasNext()
通过判断当前索引cursor与size是否相等,从而确定是否存在next元素public boolean hasNext() { return cursor != size; }
-
next()方法:
- 在访问元素前会进行modCount的检测,若modCount与expectedModCount不一致,说明list结构发生改变,将触发ConcurrentModificationEXception
- 最后,不仅返回对应位置的元素,还更新了
lastRet
(非常重要的一步操作)
public E next() { checkForComodification(); // modCount与expectedModCount是否一致 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]; // 更新lastRet }
为何使用迭代器的remove方法不会触发fail-fast机制?
-
迭代器的remove方法,代码如下
- 在删除元素后,会更新expectedModCount
- 使得expectedModCount和modCount保持一致,下一次访问时,不会触发ConcurrentModificationEXception异常
public void remove() { if (lastRet < 0) // 初始时为-1,要想不为-1,必须先通过next()获取元素 throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; // 重置为-1 expectedModCount = modCount; // 更新expectedModCount } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
-
lastRet的巧妙使用:
- lastRet初始化为-1,只有通过
next()
方法获取元素后,才会更新为具体的index - 删除元素后,会将cursor置为lastRet,也就是新的下一个元素的索引;同时,还会将lastRet重置为-1
- 这种巧妙的设计,使得想要删除某个元素,必须先通过
next()
方法获取该元素;否则,会抛出IllegalStateException
异常
- lastRet初始化为-1,只有通过
6.4 示例代码,为何不会抛出异常
-
看下面的代码:
- 按照之前的学习,删除元素2后,再次进入循环尝试获取元素3时,应该会触发异常
public static void main(String[] args) throws ParseException { ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer value = iterator.next(); // 根据分析,在访问元素3时,会触发异常 System.out.println(value); if (value.equals(2)) { list.remove(value); } } }
-
执行结果如下:既没有触发异常,又没有访问到元素3
-
原因: 巧合而已
- 通过
next()
方法获取到元素2后, cursor变成2 - 通过
list.remove()
移除元素2后,size变成2 - 在while循环的条件判断时,hasNext()将返回false,不会进入循环
- 因此,既不会触发异常,又没有访问到元素3
- 一旦被删除的不是倒数第二个元素,就会触发异常
- 通过
-
不要抱有侥幸心理,随意地在迭代时添加或删除元素,除非使用迭代器的对应方法
7. 总结
ArrayList学习结束,总结一下:
- ArrayList的特性:动态数组,支持随机访问;允许
null
值;使用fail-fast迭代器;非线程安全,对应的线程安全类Vector - ArrayList的重要属性:
Object[] elementData
数组,元素个数size
(size != 容量,即数组长度) - ArrayList的查找、添加、删除、修改方法,添加操作会触发扩容
- 扩容方法
grow()
:默认扩容1.5倍,会根据指定容量minCapacity
和最大容量MAX_ARRAY_SIZE
进行修正,最终使用native方法System.arrayCopy()
创建扩容后的新数组 - 在添加、删除元素时,大量使用native方法
System.arrayCopy()
- 何时使用rangeCheck(),何时使用rangeCheckForAdd() ?二者的区别
- for-each底层使用迭代器实现遍历,for-each遍历时修改结构,容易触发fail-fast机制(示例代码的教训)
参考链接
- ArrayList 源码分析 (大佬的、图文并茂的干货博客)
- JDK1.8源码(五)——java.util.ArrayList 类 (跟自己的学习思路大体一致的博客)
- Java 集合框架系列四:JDK 1.8 ArrayList 详解 (常见方法的代码详解)
- Java集合详解-ArrayList(短小精悍的讲解,包含扩容)
- JAVA学习-ArrayList详解
- ArrayList详解(唯一讲解了常用方法,还提到序列化的博客)