ArrayList源码分析
一. ArrayList源码分析1:怎么在源码上写注释:
在进行源码分析时,肯定有产生很多自己的理解,这个时候如果能将这些理解以注释的形式写在代码旁边就会在下次我们看源码时,给我们进行提示,那么怎么在源码上写注释呢?
1. 首先开启源码追踪设置:
2. 重新指定当前项目的引用源码
- 在当前项目的src目录外新建一个目录:
- 将本机的JDK源码src.zip解压,放到这个目录下。
-
重新指定当前项目的引用源码
3. 在源码上加注释
二. ArrayList源码分析2:实现的接口
一. 继承体系
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- 可以看到ArrayList类,直接实现的接口有4个:List,Cloneable,Serializable,RandomAcess。除了List,其他的三个接口相同点都是空接口,作为标记接口出现。
- 我们来看看这三个接口的作用:
二. RandomAccess
package java.util;
public interface RandomAccess{}
1.介绍:
RandomAccess作为随机访问的标志,代表只要实现了这个接口,就能支持快速随机访问。
2.用法:
Collections类中的binarySearch()方法:
- instanceof其作用是用来判断该对象是否为某个类或某个接口类型。所以根据判断list是否实现RandomAccess接口决定执行:Collections.indexedBinarySearch(list, key);还是Collections.iteratorBinarySearch(list, key);
2.1. Collections.indexedBinarySearch(list, key); 源码
实现RandomAccess接口的List集合采用一般的for循环遍历
private static <T>
int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
int low = 0;
int high = list.size()-1;
while (low <= high) {
int mid = (low + high) >>> 1;
//get(mid)采用一般的for循环遍历
Comparable<? super T> midVal = list.get(mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* (<tt>index < 0 || index >= size()</tt>)
*/
E get(int index);
2.2. Collections.iteratorBinarySearch(list, key);
没有实现RandomAccess接口的List集合采用迭代器遍历。
private static <T>
int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key)
{
int low = 0;
int high = list.size()-1;
ListIterator<? extends Comparable<? super T>> i = list.listIterator();
while (low <= high) {
int mid = (low + high) >>> 1;
//get(i, mid);采用迭代器遍历
Comparable<? super T> midVal = get(i, mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
/**
* Gets the ith element from the given list by repositioning the specified
* list listIterator.
*/
private static <T> T get(ListIterator<? extends T> i, int index) {
T obj = null;
int pos = i.nextIndex();
if (pos <= index) {
do {
obj = i.next();
} while (pos++ < index);
} else {
do {
obj = i.previous();
} while (--pos > index);
}
return obj;
}
3.小结:
-
ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快。
-
判断出接收的List子类是ArrayList还是LinkedList,需要用instanceof来判断List集合子类是否实现RandomAccess接口。从而能够更好地选择更优的遍历方式,提高性能。
三.Cloneable
public interface Cloneable {
}
-
介绍
Cloneable也是一个标记解耦,只有实现了这个接口后,然后在类中重写Object中的clone方法,后面通过类调用clone方法才能克隆成功,如果不实现这个接口,则会抛出CloneNotSupportedException(克隆不被支持异常)
Object中的clone方法:
protected native Object clone throws CloneNotSupportedException;
-
用法
Object中的clone方法是一个空的方法,里面也没有instanceof方法来判断类是否实现了Cloneable接口,那么clone()方法是如何判断某个类是否实现了Cloneable接口,来允许类调用clone()方法成功呢?
原因在于这个方法中有一个native关键字修饰。
1.Native关键字简单解释:
-
native关键字的函数都是操作系统实现的,java只能调用。简单地讲,一个native方法就是一个java调用非 java代码的接口。
-
java是跨平台的语言,既然是跨平台的,所付出的代价就是牺牲一些对底层的控制。但是又要一些底层功能的 作用,就需要一些其他语言的帮组,这个就是native的作用了。
-
native的意思就是通知操作系统,这个函数你必须给我实现,因为我要使用。所以native关键字的函数都是操 作系统实现的。
-
本地方法非常有用,因为它有效的扩充了JVM。事实上,我们所系的java代码已经用到了本地方法,在sun的 java并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java 运行时的界限。有了本地方法,java程序可以做任何应用层次的任务。
-
每一个native方法在JVM中都有一个同名的实现体,native方法在逻辑上的判断都是由实现体实现的。另外这 种native修饰的方法对返回类型,异常控制等都没有约束。
-
由此可见:这里判断是否实现Cloneable接口,是在调用JVM中的实现体时进行判断的。
2.JVM怎样使Native Method跑起来:
-
我们知道,当一个类第一次被使用时,这个类的字节码会被加载到内存,并且只会加载一次。
-
在这个被加载的字节码的入口维持着一个该类所有方法描述符的list(方法表),这些方法描述符包含这样一些信息:方法代码存于何处,它有奈尔参数,方法的描述符(public)等等。
-
如果一个方法描述符内用native,这个描述符块将有一个指向该方法的实现(操作系统实现的,在JVM中的同 名的实现体。)的指针。
-
这些实现在一些DLL文件内,但是他们会被操作系统加载到java程序的地址空间。
-
当一个带有本地方法的类被夹在时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。只有 当本地方法将要被调用时,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。
-
提示:使用本地方法是由开销的,它丧失了java的许多好处。如果别无选择,我们才使用本地方法。
3.浅度克隆
- 浅度克隆
- 定义一个学生类:
public class Student{
private String name;
private int age;
private StringBuffer sex;
//get,set,toString等方法
}
- 定义一个学校类,重写clone方法
public class School implements Cloneable{
private String schoolName; //学校名称
private int stuNums; //学校人数
private Student stu; //一个学生
//get,set,toString方法
@Override
protected School clone() throws CloneNotSupportedException{
return (School)super.clone();
}
}
- 写一个main方法测试:
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student();
student.setAge(18);
student.setName("小王");
student.setSex(new StringBuffer("男"));
School school = new School();
school.setSchoolName("实验小学");
school.setStu(student);
school.setStuNums(100);
System.out.println("school的hashCode:"+school.hashCode()+"\n"+
"school的student的hashCode:"+school.getStu().hashCode());
School school2 = school.clone();
System.out.println("school2的hashCode:"+school2.hashCode()+"\n"+
"school2的student的hashCode:"+school2.getStu().hashCode());
}
}
控制台结果:
浅克隆小结:
-
创建一个新的对象,新对象的非基本(引用)类型属性和原来完全相同,仍指向原有属性所指向的对象的内存地址。
-
基本类型属性例如int,在clone的时候会重新开辟一个4字节大小的空间,将其赋值。
- 多拷贝了一层
-
需要在克隆school的时候,将student(引用类型)属性也同时克隆。那么就需要Student类也实现Cloneable接口,重写clone()方法。
//1.学生类重写clone()方法 class Student implements Cloneable{ @Override protected Student clone() throws CloneNotSupportedException { return (Student)super.clone(); } } //2.克隆school对象时,也要同时克隆其student属性 @Override protected School clone() throws CloneNotSupportedException { School school = null; //此时school对象中的student属性和原来的school中的student属性是同一个。 school = (School)super.clone(); //根据原来的school中的student属性克隆一个student属性,得到一个新的student属性 //如果引用属性是Null时,就不用克隆。 if(stu != null){ school.stu = stu.clone(); } return school; }
控制台结果:
- 不能说深度克隆:这样做只是多拷贝了一层
-
如果该对象属性本身A也有对象属性B,那么B类如果没有重写clone()方法的话,那么拷贝对象A时,其对象属性B还是同一个。进行操作会同时影响到克隆对象和被克隆对象。
-
类似套娃。
4.深度克隆:序列化拷贝
-
将对象序列化转换成JSON,然后再将JSON转成指定对象。那么就会得到一个从内到外完全克隆的新的对象。和被克隆的对象完全不一样,操作其也不会影响到被克隆的对象。
-
方法1. 借助第三方jar包的API实现JSON序列化,在反序列化回来
//实现深度拷贝1:借助fastJson序列化
String s = JSON.toJSONString(father);
Father jFather = (Father)JSON.parseObject(s, Father.class);
-
方法2. java的对象输入输出流为我们提供了这种深度拷贝的实现方法。
前提是被克隆的对象及其对象属性都要实现Serializable接口。
public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(src);
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteIn);
List<T> copy_list = (List<T>) in.readObject();
return copy_list;
}
-
深度克隆小结:
也会创建一个新的对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
如果引用属性是Null时,该属性就不用克隆。
四.Serializable
public interface Serializable {
}
1.介绍
-
Serialzable也是一个标记接口,一个类只有实现了Serializable接口,它的对象才能被序列化。
-
序列化:是将对象状态转换为可保持传输的格式(字节序列)的过程。与序列化相对的是反序列化,它是将流转换为对象。这两个过程结合起来,可以轻松地存储和传输对象数据。
2.序列化用法:对象输出到文件中
- 创建一个类实现序列化接口。
class Person implements Serializable {
private String name;
private int age;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
- 序列化写到person.txt文件测试:
public class SerializableTest {
public static void main(String[] args) throws IOException {
//1.创建对象
Person person = new Person();
person.setAge(11);
person.setName("小高");
person.setSex("男");
//2.创建对象输出流
FileOutputStream fileOutputStream = new FileOutputStream("D:\\person.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
//3.调用方法将对象写入流中
objectOutputStream.writeObject(person);
//4.关闭输出流
objectOutputStream.close();
}
}
结果:
3.序列化到文件的用法步骤:
- 1.首先要创建文件字节输出流FileOutputStream对象
FileOutputStream fileOutputStream = new FileOutputStream("D:\\person.txt");
- 2.将其封装到对象操作流ObjectOutputStream对象内
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
- 3.调用writeObject()即可完成对象的序列化,并将其发送给文件字节输出流FileOutputStream对象
objectOutputStream.writeObject(person);
- 4.最后关闭流资源
objectOutputStream.close();
fileOutputStream.close();
4.Serializable的一些说明
-
对象的序列化处理非常简单,只需要对象实现了Serializable接口即可。序列化的对象包括基本数据类型,所有集合类以及其他很多东西,还有Class对象。
-
对象序列化不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保存那些对象,接着又能对每个对象内包含的句柄进行追踪。
-
使用transient,static关键字修饰的变量,在序列化对象的过程中,该属性不会被序列化。
-
但是会发现,static修饰的成员变量在序列化后紧接着反序列化,打印出来这个类的成员变量是有值的。原因是读取的值是当前JVM中的方法区对应此变量的值。
5.反序列化
- 代码:
//反序列化测试
public class DeSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException{
Person person = deSerializable();
System.out.println(person);
}
private static Person deSerializable() throws IOException, ClassNotFoundException {
//1.创建输入流
FileInputStream fileInputStream = new FileInputStream("D:\\person.txt");
//2.输入流封装到对象操作流
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
//3.调用方法读取对象
Person person = (Person)objectInputStream.readObject();
//4.关闭流
objectInputStream.close();
fileInputStream.close();
return person;
}
}
- 结果:
6.反序列化冲突
-
当创建一个类实现了Serializable接口后,会为这个类添加序列化号。当修改这个类的时候,这个类的序列化 号也会发生改变。
-
因此,在修改类前序列话;修改类之后反序列化,会校验文件的序列化号和该类的序列化号是否一致,不一致则抛出序列化号不一致的异常,也就是序列化冲突。
-
解决方案:
在指定的类中指定序列话号:
private static final long serialVersionUID = 123456789L;
-
一般序列化中的序列号冲突问题大致是这样形成的:
1.假设有一个User类,实现了Serializable接口
2.这个类编译后形成一个字节码文件,由于实现了序列化接口,在编译后的class文件会带有一个序列ID,之 后加载内存并创建对象等。
3.然后我们通过对象输出流把对象序列化写到一个文件里,这时候序列号ID也会被写入文件中。
4.最关键的一步:我们修改了源代码并重新编译,这时候序列ID就改变了。
5.再通过反序列化读取文件,它会跟新编译的字节码文件的序列ID进行比较,结果不一样,从而导致冲突。
6.所以只要我们保证即使修改了源代码,序列ID也不会发生改变就行了,所以可以给类编写一个序列号属性,只 要添加了固定的序列ID,即使源代码发生了修改,编译后的序列ID也不会改变。
三. ArrayList源码分析3:属性和方法
一. 成员变量
1. serialVersionUID
- 序列化号
private static final long serialVersionUID = 8683452581122892189L;
2. DEFAULT_CAPACITY
- 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
3. EMPTY_ELEMENTDATA:new ArrayList(0)
- 当在ArrayList的构造方法中显示的指定该ArrayList容量为0时,类内部会将EMPTY_ELEMENTDATA这个空对 象数组赋给elementData数组,返回空数组。
private static final Object[] EMPTY_ELEMENTDATA = {};
4. DEFAULTCAPACITY_EMPTY_ELEMENTDATA :new ArrayList()
-
当用户没有指定ArrayList的容量时(调用无参构造函数),类内部会将
-
DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个空对象数组赋给elementData,返回的是该数组。即ArrayList的构造方法没有显示指出ArrayList的数组长度时,类内部使用默认对象数组: DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
-
当用户第一次添加元素时,该数组将会扩容,变成默认容量为10(DEFAULT_CAPACITY)的一个数组,通过 ensureCapacityInternal实现扩容。
-
它与EMPTY_ELEMENTDATA的区别就是:该数组是默认返回的,而后者是在用户指定容量为0时返回的。
5. elementData
-
存储ArrayList元素的数组缓冲区,用该数组保存数据,ArrayList的容量就是该数组的长度。
-
当调用默认构造方法时,elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,此时第一次添加元 素进入ArrayList中时,数组将扩容为DEFAULT_CAPACITY大小。
-
是ArrayList的底层数据结构,只是一个对象数组,用于存放十几元素,并且被标记为transient,也就意味着在序列化的时候此字段是不会被序列化。
transient Object[] elementData;
5.1. 为什么该属性用transient修饰?
transient Object[] elementData;
-
一个疑问:
transient用来表示一个域不是该对象序列化的一部分,当一个对象被序列化的时候,transient修饰的变量的值是不会被序列化的。但是ArrayList又是可序列化的类,而且elementData是ArrayList具体存放元素的成员,用transient来修饰elementDate岂不是反序列化后ArrayList丢失了原先的元素?
-
原因:
elementData它是一个缓存数组,它的最大容量是Integer.MAXVALUE-8。而且它通常会预留下来一些容量,等容量不足时再扩容,那么有些空间可能就没有实际存储元素。如果每次序列化都将elementData整个序列化,包括没有存储元素的空间。反序列化时也要再初始化一个新的elementData数组,会将没有使用的空间也初始化出来,这样浪费空间。 所以为了节省空间,就让transient修饰elementData,让它不会被默认的序列化。当有序列化需求的时候,调用ArrayList自己的方法,来序列化elementData,而且只序列化实际存储的元素。
-
序列化elementData的方法:
ArrayList在序列化时会调用writeOject,直接将size和elementData写入ObjectOutputStream中。
反序列化时调用readject,从ObjectOutputStream中获取size和elementData,再恢复到elementData。
/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
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
if (size > 0) {
// be like clone(), allocate array based upon size not 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();
}
}
}
6. size
- 目前ArrayList中存放的元素的个数,默认是为0个元素。不是容量,是已经存放的元素个数。
private int size;
7. MAX_ARRAY_SIZE
-
数组缓冲区最大存储容量。
-
Java int的最大值Integer.MAX_VALUE是2147483647,ArrayList中的对象数组的最大数组容量为
Integer.MAX_VALUE-8。
-
为什么要减去8:JVM会在这个数组中存储一些数据。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
8. modCount
protected transient int modCount = 0;
-
继承于AbstractList,不是直接定义在ArrayList类里面的属性。
-
这个成员变量记录着集合的修改次数,也就是每次add或者remove它的值都会加1.
二. 构造方法
1. 无参构造
/**
* Constructs an empty list with an initial capacity of ten.
* 构造一个初始容量为0的ArrayList,数组缓冲区elementData长度为0
* 当第一次向其添加元素时,会在add方法中扩容至默认容量10.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2. 有参构造:initialCapacity
/**
* Constructs an empty list with the specified initial capacity.
* 构造一个具有指定初始化容量的空列表
* @param initialCapacity the initial capacity of the list
* 参数: 列表的初始化容量
* @throws IllegalArgumentException if the specified initial capacity
* is negative
* 当指定的容量小于0时,抛出参数非法异常。
*/
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);
}
}
3. 有参构造:传入集合
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
* 构造一个集合包含指定集合的元素,按照由原集合的迭代器返回的顺序排列。
*
* @param c the collection whose elements are to be placed into this list
* 参数: 指定的集合,要将该集合中的元素放入新的集合中。
* @throws NullPointerException if the specified collection is null
* 当参数集合为null时,抛空指针异常。
*/
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;
}
}
分析1:elementData = c.toArray();
- 1.将传入的集合转成数组,赋值给elementData。注意elementData是一个Object[] 引用,但是指向c.toArray()返回的结果,c.toArray()返回的结果却可能是其他类型(Object[]子类),所以此时的elementData类型可能不是Object[]
elementData = c.toArray();
分析2:elementData.getClass() != Object[].class
-
elementData.getClass() != Object[].class成立的情况:
list.toArray()方法返回结果不一定是Object泛型的ArrayList<>。调用Arrays.asList()方法将数组转换成集合时返回的是Arrays的内部类ArrayList,而且是一个泛型集合。
- 那么当泛型类型是String时,得到的内部类类型:ArrayList:
String[] strArray = {"a", "b", "c"};
List<String> list = Arrays.asList(strArray); //返回的不是ArrayList,是Arrays的内部类ArrayList
- 所以此时再将list.toArray();转成数组,得到的就是String[] 不是Object[],上述条件就成立了。
String[] strArray = {"a", "b", "c"};
List<String> list = Arrays.asList(strArray); //返回的不是ArrayList,是Arrays的内部类ArrayList
Object[] array2 = list.toArray();
System.out.println(array2.getClass());
//输出结果:
class [Ljava.lang.String; //String[]数组
分析3. 小结:
- 通过Arrays.asList(array)转换得到list保留了原本的类型,list.toArray()再将其转成数组时得到的是原来类型的数组。
String[] strArray = {"a", "b", "c"};
List<String> list = Arrays.asList(strArray); //返回的不是ArrayList,是Arrays的内部类ArrayList
Object[] array2 = list.toArray();
System.out.println(array2.getClass());
//输出结果:
class [Ljava.lang.String; //String[]数组
- 而通过ArrayList arrayList = new ArrayList();方式创建的ArrayList是真正的java.util.ArrayList,它的toArray是返回了Object[]的。
分析4:整体逻辑
- 将传入的集合转成数组,因为ArrayList的底层就是用数组存储,用数组实现的。
elementData = c.toArray();
- 将传入的集合参数的长度赋值给size,实际ArrayList中存放的元素的个数。并判断是否为0.
(size = elementData.length) != 0
- 如果为0,说明传入的是空集合,将object[ ] EMPTY_ELEMENTDATA赋值给elementData。类似调用传入参数为0的构造方法new ArrayList(0),指定该集合初始长度为0。
this.elementData = EMPTY_ELEMENTDATA
- 如果不为0,说明传入的集合是有元素的,那么构建的新的ArrayList集合要包含这些元素。
-
3.1. 先判断传入的集合转成数组赋值给了Object[ ] elementData数组后,其类型有没有改变。可能变成了其他子类型数组,比如String [ ]。常见的原因是Arrays.asList(array),得到的是Arrays的内部类ArrayList。
if (elementData.getClass() != Object[].class)
-
3.2. 如果elementData不是Object[ ]类型,那么就要重写构造一个String [ ],并且长度是传入集合的长度,类型是String [ ],且包含传入的集合的原有元素。
elementData = Arrays.copyOf(elementData, size, Object[].class);
- 这样elementData Object [ ]数组就初始化好了。
分析5. 补充
-
调用Arrays.asList()把数组装换成集合,返回的是Arrays的内部类ArrayList,而不是java.util.ArrayList。
-
Arrays的内部类ArrayList和java.util.ArrayList都是继承AbstractList,而AbstractList中的add/remove等方法是默认throw UnsupportedOperationException的。
-
但是java.util.ArrayList重写了这些方法,而Arrays的内部类ArrayList没有重写,所以由asList()得到的ArrayList调用add/remove等方法会抛出异常。
三.从添加元素:看添加方法和扩容方法
//测试代码
ArrayList arrayList = new ArrayList();
arrayList.add("a"); //第一次会扩容
arrayList.add("b"); //第二次不会扩容
arrayList.add("c");
1.默认无参构造方法:new ArrayList();
/**
* 默认构造方法
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() { //默认初始化构造器
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
- 当用户没有指定ArrayList的容量时,调用无参构造函数。且类内部会将defaultcapacity_empty_elementData这个空对象数组赋值给elementData这个数组对象引用。
2.第一次添加元素(扩容):添加方法
//测试代码
arrayList.add("a");
//添加方法源码
public boolean add(E e) {
//minCapacity:要满足elementData地最小长度。确定容量是否够,不够的话扩容 Increments modCount!!
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
1.1 分析:add(E e)
-
调用ensureCapacityInternal(size + 1);
size是当前集合(底层数组)的有效长度(有几个数据)。size+1是代表着新添加元素时,底层数组需要的最小容量。 所以ensureCapacityInternal(size + 1);就是判断当前数组长度(不是有效长度,是容量),是否满足elementData进行此次添加时需要的最小容量(当前有效长度size+1)。
//确认数组需要的最小容量
//minCapacity:要满足此次添加操作,elementData需要的最小长度
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//elementData是不是一个空数组
//设置minCapacity为DEFAULT_CAPACITY=10和minCapacity中的最大值,第一次扩容返回10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
1.2 分析:ensureCapacityInternal(size + 1);
-
作用:确认此次操作elementData 需要的最小容量。
-
判断elementData是不是一个空数组,elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA。是不是采用默认构造得来的(默认构造,defaultcapacity_empty_elementData是一个空数组,长度为0)。
//再回顾一下DEFAULTCAPACITY_EMPTY_ELEMENTDATA属性
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
- 显然此时满足条件:取默认容量和minCapacity参数中的最大值,为赋给最小容量minCapacity。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
- 调用ensureExplicitCapacity(minCapacity);
1.3 ensureExplicitCapacity(minCapacity);
- 作用:如果当前数组总容量小于此次操作需要的有效长度,调用扩容方法。
//如果最小容量大于当前数组长度:调用扩容方法grow(minCapacity)
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);//扩容
}
- 显然第一次插入时,此时当前数组是空数组,长度为0,而需要的最小长度为1,所以调用扩容方法。
1.4 grow(int minCapacity)
- 作用:核心扩容方法
//扩容方法:有一个小算法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//右移一位,除以进制2。在加上oldCapacity,所以相当于扩容为当前长度的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//新的容量小于最小需要的容量,将最小需要的容量赋值给新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//长度是不是超过最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// minCapacity通常接近大小,所以这是一个胜利
// 数组拷贝得到新数组:有新的长度和原来的元素
elementData = Arrays.copyOf(elementData, newCapacity);
}
-
在此处为ArrayList集合扩容。
1.先定义一个变量,旧容量oldCapacity,等于当前数组的总容量。
2.再定义一个变量,新容量newCapacity,等于当前容量的1.5倍:
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
注意,因为是第一次add,所以oldCapacity=0,所以1.5被后,newCapacity还是0。所以还要进行一个判断
3.当新的容量小于最小需要的容量(第一次add时会出现该情况),将最小需要的容量赋值给新容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
- 4.如果新的容量超过了数组缓冲区最大存储容量,则调用hugeCapacity(minCapacity);来决定洗呢容量大小。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
-
5.最后容量确定下来,扩容完成。
根据当前的elementData已经有的元素和新的容量,构造一个新的elementData数组。 最后将旧数组拷贝到扩容后的新数组中。
elementData = Arrays.copyOf(elementData, newCapacity);
1.5 hugeCapacity(minCapacity);
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 作用:确保不小于0并且不超过最大容量。小于0报错,大于0返回Integer.MAX_VALUE。
3.扩容机制小结:grow(int minCapacity)
-
在add()方法中调用ensureCapacityInternal(size+1)来确定此次添加元素,elementData数组需要满足的最小容量。size+1代表此次添加元素时,需要满足的最小容量或者添加成功之后的实际容量。
-
ensureCapacityInternal(minCapacity)方法会通过:
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)来判断elementData 是否是默认方法构造的空数组。如果是,意味着此次添加时空数组第一次添加元素,那么直接返回默认容量给minCapacity。默认方法构造的集合,第一次扩容到容量10。
-
ensureCapacityInternal调用ensureExplicitCapacity(minCapacity)方法来确定,当前数组总容量是否小于此次添加操作需要的最小容量,如果是的话就要扩容,调用扩容方法。
首先将结构性修改计数器modCount加1,然后判断minCapacity与当前数组总容量的大小,如果大于当前数组总容量的大小,就需要扩容。
-
扩容方法:grow(int minCapacity)
参数minCapacity表示确保此次添加成功需要的最小容量。然后先不管这个minCapacity,将当前数组总长度扩大1.5倍得到newCapacity。然后将扩容后的数组容量与minCapacity比较:
A:newCapacity < minCapacity:0的1.5倍还是0,排除首次添加的扩容情况,直接将minCapacity指定为新的容量。
B:newCapacity > minCapacity:指定newCapacity 为新的容量。
// minCapacity is usually close to size, so this is a win:minCapacity通常接近大小,所以这是一个胜利
C:最后将旧数组拷贝到扩容后的新数组中。
四.其他方法
1. 获取当前集合大小:有效元素个数
注意:集合底层的elementData是被封装起来的,其容量也是被封装的,所以看不到。
public int size() {
return size;
}
2. 判断集合是否为空
public boolean isEmpty() {
return size == 0;
}
3. 判断元素对象是否存在集合当中
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//从头遍历集合,找到相同的元素
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;
}
- 注意:AaaryList是可以存null的,而null元素和非null元素的等值判断是不同的:
elementData[i]==null //null元素的判断:==
o.equals(elementData[i] //非null对象的判断:equals
-
如果有两个不同的对象,显然它们的地址值不一样,==判断为false。但是当他们的属性,内容都一样时,我们想要得到的判断结果是true,所以此时就需要在类中重写equals,用equals来比较对象的内容。
-
为什么一般建议重写equals的同时重写hashCode,因为equasl判断true的两个对象其hashCode判断可能是false。为了避免这个情况,我们重写hashCode方法,让equasl判断true的两个对象其hashCode判断也是true。
- 做个实验:有两个对象且属性值一样,但是list只存入1个对象
//测试代码
ArrayList arrayList1 = new ArrayList();
Test1 test1 = new Test1();
test1.setAge(1);
test1.setName("ttt");
Test1 test2 = new Test1();
test2.setAge(1);
test2.setName("ttt");
arrayList1.add(test1);
System.out.println(arrayList1.contains(test1));
System.out.println(arrayList1.contains(test2));
//类Test1:
public class Test1 {
private int age;
private String name;
//get/set省略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Test1)) return false;
Test1 test1 = (Test1) o;
return getAge() == test1.getAge() &&
Objects.equals(getName(), test1.getName());
}
@Override
public int hashCode() {
return Objects.hash(getAge(), getName());
}
}
- 结果:
- 说明:ArrayList判断集合中的对象元素是否存在时,要根据该对象所在类是否重写了equals来的到结果。
4. 从尾部循环查找相同的元素,返回下标
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;
}
5. 浅拷贝:super.clone();
/**
* Returns a shallow copy of this <tt>ArrayList</tt> instance. (The
* elements themselves are not copied.)
*
* @return a clone of this <tt>ArrayList</tt> instance
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
//rrayList的Object[ ] elementData成员变量不会被克隆,要为它单独进行一次克隆
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
-
浅拷贝意味着:
会复制对象及其内所有基本类型成员和String这种不可变成员类型,但是不会赋值对象成员变量。所以ArrayList的Object[ ] elementData成员变量不会被克隆,要为它单独进行一次克隆
v.elementData = Arrays.copyOf(elementData, size);
6. 集合转成数组:无参
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
-
返回的数组是“安全的”,因为toArray()返回的是一个新的数组对象,因此调用者可以自由地修改返回的数组,不会影响到list本身原来存储的元素值。
-
此方法充当基于数组和集合的api之间的桥梁。
7 集合转成数组:有参
@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(T[] a)有参方法,需要传入一个数组作为参数,并且通过泛型T的方式定义参数类型。所返回的数组类型就是调用者的泛型,所以自己无需再转型。只是根据传入数组的长度与集合的实际长度的关系,会有不同的处理:
- a. 数组长度不小于集合长度,直接拷贝,不会产生新的数组对象。并在a[size] = null;size就是list有效元素的大小,这个null值可以使得toArray(T[] a)方法的调用者可以判断出null后面就没有元素了。
System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null;
- b. 数组长度小于集合长度,会创建一个与集合长度相同的新数组,并且将集合的元素拷贝到新数组并将新数组的引用返回
if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass());
8. 位置访问
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
-
注意:该方法的修饰符是default:说明该方法只能在当前类和同一包下访问。
-
protected:除了当前类以及同一个包下访问外,还为不在同一个包下的子类提供了一种访问父类成员的方式
9. 返回指定位置的元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
10. 将集合中指定位置的元素替换为指定元素,返回的是被修改前的元素。
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
11. 在指定位置插入元素:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
-
如果该索引处以及后面都有元素,那么将他们索引都往后移动一位。
-
原生方法arraycopy的五个参数:
-
1.src: 要被复制的数组elementData
-
2.srcPos: 要被复制的数组开始赋值的下标index
这两个参数确定移动的元素的下标:index~size-1
-
3.dest: 目标数组,也就是要把数据放进来的数组
-
4.destPos: 从目标数组的第几个下标开始放数据。
-
5.length: 表示从赋值的数组中拿几个元素放到目标数组中。
这两个参数确定要移动的元素放置到目标数组的位置。
-
12. 删除方法:
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;
}
-
先判断数组下标是否越界,取出要删除的元素。将删除位置之后的元素向前移动,将size位置出的值设为null,size-1,返回旧值。
-
将删除位置之后的元素向前移动:假设index=4,size=10 index: 0~9
-
假如删除index=4号下标的元素,那么index+1=5 ~ size-1的元素都要向前移动一位,放置到index =4~ size-2的位置上。并且将size-1位置处赋为null。
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;
}
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
}
13. 清空元素
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
14. 将参数集合中的元素,添加到集合中,从index下标出插入。
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);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
15. 删除指定范围的元素:
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;
}
16. 删除集合参数中的交集元素:removeAll
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
//Object中的方法
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
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;
}
分析一下这个方法:
-
首先判断一下传入的集合是否为空
Objects.requireNonNull(c);
-
调用批量移除方法:batchRemove(c, false);
return batchRemove(c, false);
-
分析一下:batchRemove(Collection<?> c, boolean complement)方法
//retainAll:complement=true ; removeAll:false 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++) //complement为false时,取出所有的差集:就是不要删除的元素 if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws.表示可能出错了 if (r != size) { //r!=size表示可能出错了:w表示要保留的元素个数,且已经存好了。那么出错时,说明r下标之后的元素还没有判断要不要保留。那么此时出错了,就将之全保留。 //复制elementData的下标r到末尾的元素(还没有判断是否要保留的余下元素),放到elementData数组的w下标到w+size-r-1(后面的元素当作全部要保留) System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { //w == size:说明elementData的所有元素都需要保留,所以集合里面的元素没有被删除,也即原集合与参数c集合没有交集元素 // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true;//w == size时,走不到这里。modified=false,说明数组没有被修改。 } } return modified; }
-
3.1. 几个变量
final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; //标志集合是否被修改,且返回该标志
a. final Object[] elementData = this.elementData;
这是一个final 修饰的引用变量,注意此时final 不可变的含义是引用变量elementData 不可变,即地址值,指向的对象的地址不变。但是对象的内容是不被约束的。
b. boolean modified = false
标志集合是否被修改,且返回该标志。
-
3.2. 具体方法
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;
-
a. c.contains(elementData[r]) == complement
for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r];
此时是removeAll方法调用的batchRemove方法,传入的参数complement是false,所以上面这段代码的含义,遍历所有elementData的元素,保留c.contains(elementData[r]结果为false的元素。即保留在elementData中存在但c中不存在的元素,这些元素是不要删除的。删除参数c中有的同时也在elementData中有的元素。
保存代码:elementData[w++] = elementData[r];
同时没保存一个不需要删除的元素,w加1。
-
b. if (r != size)
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; } }
按理说将elementData遍历完,r == size。那么出现r != size的情况,意味着可能在for代码中抛出了异常,导致elementData没有遍历完。
elementData没有遍历完的情况:遍历到下标为r的元素,r之前的元素需要保留的都保留了下来,r之后的元素还没有判断。那么此时由于报错,r后面还没有判断的元素就都不要随意删除了,先全部复制下来,复制到保留的元素的后面。
复制r后面元素的代码:
//取elementData从r下标开始的元素,长度size - r,放到elementData中从w位置开始放。 System.arraycopy(elementData, r, elementData, w, size - r);
且此时保存的元素个数w:w += size - r;
-
c. if (w != size)
w == size时,意味着保留的元素个数w等于elementData的总元素个数,保留了elementData的所有元素。所以也就意味着c集合的元素中没有在elementData存在的元素,也即没有删除元素。此时集合的修改标志:modified没有赋值,没有修改,且将其返回。
w != size时,意味着保留的元素格式小于elementData的总元素个数,那么其他的元素就是要删除的元素。将这些位置的值置为null,并且置集合的修改标志:modified=true;并且返回。
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; }
17 保留集合参数中的交集:retainAll
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
-
注意:此时调用batchRemove(c, true);传入的参数为true,所以此时的for循环判断:
for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r];
-
保存即存在与elementData也存在于参数c中的元素,其他的和removeAll类似。
五. ArrayList迭代器源码实现
1. 迭代器的接口定义:java.util.Iterator
package java.util.Iterator;
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
-
可以看到迭代器中只定义了四个方法:
a.hasNext()判断是否有下一个元素
b.Next()获取一下个元素
c.remove()删除元素
d.forEachRemaining()对每个剩余元素都执行给定操作,直到所有元素都被处理完,或者有动作引发异常。
2. ArrayList中的迭代器实现源码
- 内部类:private class Itr implements Iterator
//一般用来初始化得到迭代器的方法:Iterator iterator = arrayList2.iterator();
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
* ArrayList中的迭代器实现源码:相比AbstractList.Itr经过优化的迭代器版本。
*/
private class Itr implements Iterator<E> {
int cursor; // 指向下一个元素的游标:index of next element to return
int lastRet = -1; // 指向最近返回元素的下标:index of last element returned; -1 if no such
//ArrayList的修改次数,每次对ArrayList内容的修改都会增加该值。
int expectedModCount = modCount;
//判断是否有下一个元素:cursor不等于size时,表示还有下一个值
public boolean hasNext() {
return cursor != size;
}
//获取下一个元素
@SuppressWarnings("unchecked")
public E next() {
//检查ArrayList是否被非迭代器方法修改了
checkForComodification();
//当前的游标赋给i
int i = cursor;
//判断游标是否越界:size是集合list的长度
if (i >= size)
throw new NoSuchElementException();
//内部类访问外部的属性:类名.this
Object[] elementData = ArrayList.this.elementData;
//length是elementData数组的长度
if (i >= elementData.length)
//为什么当游标i>=elementData数组的长度时,会报Fail-Fast异常呢?
throw new ConcurrentModificationException();
//游标+1:指向下一个元素
cursor = i + 1;
//返回下一个元素,并将lastRet指向最近一次获取的元素的下标
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
//判断集合版本号与集合迭代器版本号是否相同
checkForComodification();
try {
//调用ArrayList的remove方法
ArrayList.this.remove(lastRet);
//注意cursor比lastRet大一,所以此操作,将cursor游标指针往回移动一位
cursor = lastRet;
//因为是删除操作,没有返回数据,将最近一次返回数据的下标置为-1。
lastRet = -1;
//重新设置expectedModCount避免ConcurrentModificationException异常
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
2.1. 单独看一下:forEachRemaining()
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
//将list中的所有元素都给了consumer,可以用这个反复噶取出元素。
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
-
这个方法是Java1.8新增的Iterator接口中的默认方法,对于这个方法官方文档的描述:对集合中剩余的元素进行操作,直到元素完毕或者抛出异常。
-
那么怎么理解剩余元素呢?看一段代码:
ArrayList arrayList2 = new ArrayList();
arrayList2.add(3);
arrayList2.add(4);
arrayList2.add(7);
arrayList2.add(5);
arrayList2.add(6);
arrayList1.removeAll(arrayList2);
Iterator iterator = arrayList2.iterator();
int i = 0;
while (iterator.hasNext()){
System.out.println(iterator.next());
i++;
if(i == 2){
break;
}
}
System.out.println("========================");
iterator.forEachRemaining(new Consumer() {
@Override
public void accept(Object o) {
System.out.println(o);
}
});
- 所以看过Itr的源码后就很好理解剩余元素了,剩余元素就是ArrayList的内部迭代器实现类Itr的指向下一个元素的游标属性cursor之后的元素。
2.2 Fail-Fast 机制
-
- modCount是修改次数,每次对ArrayList内容的修改都将增加这个值
modCount主要是为了防止在迭代过程中通过List的方法(非迭代器中的方法)改变了原集合导致出现了不可预料的情况,从而提前抛出并发修改异常。注意是:“提前”,在可能出现错误的情况下提前抛出异常终止操作。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
-
- 下面这段代码就会抛出ConcurrentModificationException异常:
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
arrayList.add(i);
}
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
arrayList.remove(1);
iterator.next();
}
-
- 异常原因:
当进行迭代器遍历是,我们要用iterator.next()来获取下一个元素。通过iterator.next()的源码可以发现它是通过游标cursor返回其指向的元素的:
int i = cursor; cursor = i+1; //继续指向下一个元素 return elementData[lastRet = i];
所以如果此时cursor = 3,那么正常的话返回下标是3的元素比如:
此时在迭代器遍历的过程中调用的arrayist.remove删除了下标为1的元素
while (iterator.hasNext()) { arrayList.remove(1); iterator.next(); }
此时集合结果:
遍历的结果:本来要遍历元素3的,却遍历了元素4,显然对于遍历操作来说这是错的。
因为这个原因,在迭代器初始化的过程中会将modCount赋值给expectedModCount,在迭代的过程中判断这两者是否相等,如果不相等,就表示修改了ArrayList,在继续用迭代器操作ArrayList就很可能发生错误。所以提前报错:throw new ConcurrentModificationException();
-
- 用迭代器Iterator时,可以用迭代器自带的remove等操作集合的方法,此时也不会影响迭代器的操作。因为他会在改变集合的同时将游标cursor做出相应的移动。
public void remove() { if (lastRet < 0) throw new IllegalStateException(); //判断集合版本号与集合迭代器版本号是否相同 checkForComodification(); try { //调用ArrayList的remove方法 ArrayList.this.remove(lastRet); //注意cursor比lastRet大一,所以此操作,将cursor游标指针往回移动一位 cursor = lastRet; //因为是删除操作,没有返回数据,将最近一次返回数据的下标置为-1。 lastRet = -1; //重新设置expectedModCount避免ConcurrentModificationException异常 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
3. 内部类:private class ListItr extends Itr implements ListIterator
-
介绍:
这也是ArrayList一个迭代器实现类,相比Itr扩展了一些功能。
它可以进行双向移动,而普通的迭代器只能单向移动。
它有添加add方法,而后者没有。
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
public ListIterator<E> listIterator() {
return new ListItr(0);
}
/**
* An optimized version of AbstractList.ListItr
*/
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}