参考资料:B站
文章目录
1. ArrayList继承体系
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
-
Serializable:类的序列化由实现java.io.Serializable接口的类启用。不实现此接口的类将不会使任何状态序列化或反序列化。可序列化类的所有子类型都是可以序列化的。序列化接口没有方法和字段,仅用于标识可串行化的语义,也就是标记接口,告诉jvm说我可以序列化,类似注解。该接口如下:
public interface Serializable { }
序列化:是将对象的状态信息转换为可以存储或传输的形式的过程。比如将对象序列化到硬盘,方便网络传输。
反序列化:将硬盘中某个文件中的数据读取出来,反序列化为一个对象。
注意:如果该类实现了序列化接口,那么该类里的所有属性都会被序列化和反序列化,不想序列化的字段可以使用transient修饰。 -
Cloneable:它也是标记接口,一个类实现Cloneable接口来指示Object.clone()方法。在不实现Cloneable接口的实例上调用对象的克隆方法会导致异常CloneNotSupportedException被抛出。所谓的克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝。
克隆的前提条件:1. 被克隆对象所在的类必须实现Cloneable接口。
2. 必须重写clone()方法。注意clone()方法是Object的。
基本使用://普通类实现了Cloneable接口 public class Student implements Cloneable{ private String name; private String sex; @Override protected Object clone() throws CloneNotSupportedException { //调用Object的clone方法,注意它是native方法,是调用底层C语言的 return super.clone(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", sex='" + sex + '\'' + '}'; } }
public static void main(String[] args) throws CloneNotSupportedException { Student stu = new Student(); stu.setName("小霆"); stu.setSex("男"); //调用克隆方法,复制出一个一模一样的自己 Object stu1 = stu.clone(); System.out.println(stu == stu1);//false 两个是不同的对象,是相互独立的 //对比复制前的内容跟复制后的内容是否一样 System.out.println(stu); //Student{name='小霆', sex='男'} System.out.println(stu1); //Student{name='小霆', sex='男'} //答案是一样的 }
如果说我Student类不去实现Cloneable接口的话,如下:
Exception in thread "main" java.lang.CloneNotSupportedException: thread.Student at java.lang.Object.clone(Native Method)
分类:clone可以分为浅拷贝和深拷贝。先看浅拷贝,有如下代码:
//技能类 public class Skill { private String skillName; public Skill(){ } public Skill(String skillName){ this.skillName = skillName; } public String getSkillName() { return skillName; } public void setSkillName(String skillName) { this.skillName = skillName; } @Override public String toString() { return "Skill{" + "skillName='" + skillName + '\'' + '}'; } }
public class Student implements Cloneable{ private String name; private String sex; private Skill skill; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public Skill getSkill() { return skill; } public void setSkill(Skill skill) { this.skill = skill; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", sex='" + sex + '\'' + ", skill=" + skill + '}'; } }
注意Student类做了改动,里面包含了Skill对象。
有如下测试:
也就是说,当skill的值发生了改变,被克隆的对象stu1的skillName属性值也发生改变。这说明了该拷贝拷的是skill对象的引用。按道理说,我们拷贝一个东西,如果原东西发生改动,是不是不能影响拷贝过来的东西。像这种情况就叫浅拷贝。那么深拷贝能解决这个问题吗?如下,先让Skill类也实现Cloneable接口,并重写clone()方法,这个就不贴代码出来了,然后对Student类的clone()方法做个修改:
@Override protected Object clone() throws CloneNotSupportedException { //return super.clone(); 深拷贝,不能简单的调用父类的方法 Student stu = (Student) super.clone(); Skill skill = (Skill) this.skill.clone(); stu.setSkill(skill); return stu; }
再做测试,如下:
所以,我们可以做个总结,浅拷贝是不会拷贝里面的子对象的,而深拷贝会把里面的子对象也拷贝一份,这样,两者所引用的技能对象的地址就不同了。 -
RandomAccess(随机访问)标记接口:表示了实现该接口的类支持快速随机访问。此接口的主要目的是允许通过算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。那什么是随机访问呢?说的简单点,就是可以通过索引去定位一个元素,比如如下的for循环:
ArrayList arrayList = new ArrayList(); arrayList.add("a"); arrayList.add("b"); for (int i=0;i<arrayList.size();i++){ System.out.println(arrayList.get(i)); }
那么顺序访问就是从头到尾依次访问,比如链表的访问方式就是顺序访问。如下的iterator就是顺序访问(当然也包括增强for循环,毕竟增强for循环底层也是iterator):
ArrayList arrayList = new ArrayList(); arrayList.add("a"); arrayList.add("b"); Iterator iterator = arrayList.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); }
然后看一下ArrayList的随机访问和顺序访问哪个的效率高,如下:
public static void main(String[] args) throws CloneNotSupportedException { ArrayList arrayList = new ArrayList(); for (int i=0;i<100000;i++){ arrayList.add(i); } long start = System.currentTimeMillis(); for (int i=0;i<arrayList.size();i++){ arrayList.get(i); } long end = System.currentTimeMillis(); System.out.println("随机访问所用时间"+(end-start)); System.out.println("======================"); long start2 = System.currentTimeMillis(); Iterator iterator = arrayList.iterator(); while (iterator.hasNext()){ iterator.next(); } long end2 = System.currentTimeMillis(); System.out.println("顺序访问所用时间"+(end2-start2)); }
经过多次测试,发现用for循环所用的耗时要么比iterator所用的耗时少,要么相等,反正就是不会大于iterator所用的时间,所以说用for循环的这种随机访问的效率更高。
然后我们再看看链表的吧,代表是linkedList(没有实现RandomAccess),看看它是随机访问快,还是顺序访问快,如下:public static void main(String[] args) throws CloneNotSupportedException { List linkedList = new LinkedList(); for (int i=0;i<100000;i++){ linkedList.add(i); } long start = System.currentTimeMillis(); for (int i=0;i<linkedList.size();i++){ linkedList.get(i); } long end = System.currentTimeMillis(); System.out.println("随机访问所用时间"+(end-start)); System.out.println("======================"); long start2 = System.currentTimeMillis(); Iterator iterator = linkedList.iterator(); while (iterator.hasNext()){ iterator.next(); } long end2 = System.currentTimeMillis(); System.out.println("顺序访问所用时间"+(end2-start2)); }
这次换成了LinkedList之后,随机访问所耗的时间就比顺序访问所耗的时间多了多了。
所以,以后我们再进行List遍历的时候,就要小心了,是采用随机遍历还是顺序遍历呢?我们不妨在遍历之前先查询返回的结果是否实现了RandomAccess接口,如果实现,就推荐使用随机遍历的方式,否则,就推荐使用顺序遍历的方式。如下:if(arrayList instanceof RandomAccess){ //随机访问 }else{ //顺序访问 }
-
AbstractList抽象类:该抽象类实现了List接口,并对一些通用的方法做了重写,所以我们可以采用它的默认实现,直接用,如果想自己重写那就自己重写。
2. ArrayList的构造函数
Constructor | 描述 |
---|---|
ArrayList() | 构造一个初始容量为10的空列表 |
ArrayList(int initialCapacity) | 构造具有指定初始容量的空列表 |
ArrayList(Collection<? extends E> c) | 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序 |
2.1 无参构造
如下代码:
public static void main(String[] args){
//它真的构造了一个初始容量为10的空列表吗?
ArrayList arrayList = new ArrayList();
}
让我们按住Ctrl键,然后鼠标移到ArrayList上,点进去,进入如下代码:
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
我们一眼看上去,是不是一个赋值操作呀。让我们找到elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
吧,如下:
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
我们发现DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是一个常量,类型是Object[],值是{},也就是空数组,我们对其进行直译,就是默认的空容量数组,它的值是不能变的。
elementData呢类型也是Object[],说明它也是数组,注意这个数组才是集合真正存储数据的容器,也就是说集合的容量(容量是容量,size是size,不是一回事)就是该数组的长度,并且我们都知道ArrayList的底层是数组,那么这个数组就是elementData。我们发现没有,它前面有transient修饰,什么意思,我前面说了,这里不再重复说了。那么此时elementData就是空数组。嗯,怎么没初始化容量为10呢,上面不是说无参构造的初始容量为10吗?其实初始容量为10是当我们第一次add的时候才初始化的。
2.2 有参构造
如下代码:
public static void main(String[] args){
ArrayList arrayList = new ArrayList(10);
}
进入源代码,如下:
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);
}
}
这个很容易,我们是不是传入了一个10,那么10的传入,就要进行if判断,首先10是不是大于0,那么第一个成立,其它的像else就不走了,那么第一个判断的方法体是不是也是赋值操作,elementData我们已经知道是什么了,而等号右边是不是也非常简单呀,等价替换为:Object[] elementData=new Object[10]
,所以,是不是初始化了一个容量为10的空数组呀,如果我们传入的是20,是不是就初始化容量为20的数组呀。
如果我们传入的是0,那么就会进入第二个判断的方法体里,让我们点进EMPTY_ELEMENTDATA
,如下:
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
是不是一个空数组呀,也是常量,跟上面的那个DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是一样的,只不过EMPTY_ELEMENTDATA
这个是当我们指定为0才给我们返回的,而前面带有DEFAULTCAPACITY_
这个单词是不是翻译为默认容量呀,默认默认,就是默认给我们的,就是当我们不指定为0的时候,它就拿DEFAULTCAPACITY_EMPTY_ELEMENTDATA
给我们。好,最后不管怎么样,elementData是不是为空。
2.3 有参构造之单列集合
public static void main(String[] args){
ArrayList arrayList = new ArrayList();
arrayList.add("aaa");
arrayList.add("bbb");
arrayList.add("ccc");
ArrayList arrayList1 = new ArrayList(arrayList);
for(Object s:arrayList1){
System.out.println(s);
}
}
定位到第6行,进去:
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;
}
}
首先,集合调用toArray方法,就是把集合转换为数组,注意,它的底层是Arrays.copyOf,所以它其实是创建了一个新数组,该新数组的长度跟集合的长度是一样的,并且值也是一样的,就是复制了一份嘛。注意哈,我并没有说容量一样。如下:
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
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));
return copy;
}
首先,定位到上述源代码的第3,4,5行上,认真观察就会发现等号右边的是三目运算,并且看结果可以知道,不管结果如何,它都会创建一个新的数组,这个数组跟我们的elementData可是不一样的,也就是说是两个对象。好,然后通过参数可知,它们都有newLength的参数,这个newLength就是集合的长度size,所以为3,也就是说,它创建了一个Object类型的数组,容量为3。也就是这样的,如下:
Object[] copy = new Object[3];
注意变量我用copy接收。
再来看看System.arraycopy是什么东西,如下案例:
public static void main(String[] args) {
//arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
/*
Object src : 原数组
int srcPos : 从元数据的起始索引开始
Object dest : 目标数组
int destPos : 目标数组的开始起始索引
int length : 要copy的数组的长度
*/
int[] array = new int[10];
for(int i=1;i<=5;i++){
array[i-1] = i;
}
for(int i=0;i<array.length;i++){
System.out.print(array[i]+",");
}
System.out.println();
System.arraycopy(array,1,array,2,4);
for(int i=0;i<array.length;i++){
System.out.print(array[i]+",");
}
}
运行结果如下:
1,2,3,4,5,0,0,0,0,0,
1,2,2,3,4,5,0,0,0,0,
我们通过画图来分析上述结果是怎么出来的,如下:
然后我们把源代码的等价替换过来,如下:
System.arraycopy(elementData, 0, copy, 0,
Math.min(elementData.length, 3));
Math.min说一下,它是对比两个参数,看两个参数哪个小,就返回谁。
一样画图分析,如下:
总结:1. 先把新数组创建出来。2,再用System.arraycopy进行赋值。
最终是不是一直把copy数组返回出去呀,返回到哪?就是如下这句:
elementData = c.toArray();
那么此时的elementData就是新数组,也就是那个copy。然后往下,就是判断了,它会取新数组的长度,然后赋值给size,再判断它是否并不等于0,如果不等于0,那么进去,然后下面这个判断是为了防止c.toArray()返回的不是Object[].class,也就是说,我们传进来的arrayList很有可能重写了toArray()方法,并且toArray()返回的不是Object[]类型的,而是String[]类型之类的,这样的话,如果遇到向下转型的话就会报错。到此为止,arrayList1的elementData就有值了,所以arrayList1这个集合就复制出来了。
3. add方法
3.1 add(E e)
如下代码:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("aaa");
}
定位到第3行,进入源码,如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
size为0,注意这个size表示集合的长度,此时集合里还没有元素,字符串aaa还没添加进去呢,它执行到第二行才会添加进去,很明显是不是size++呀,这个很简单,重点放在第一行上,让我们进去ensureCapacityInternal方法,如下:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
上面已经见到过了,就是来判断elementData是否为空数组,或者说你这个elementData数组的容量是否为空,这样说更准确,因为上面我们是不是学过ArrayList的无参和有参?,而且在无参那节我是不是说过它会初始化一个容量为10的数组,但是是在add的时候才初始化的,还记不记得?那就没错了,看我的案例,是不是new了一个无参的ArrayList,所以容量就为空,我再把无参的源代码贴出来,免得你又翻回去:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
看到没有,DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是不是赋值给elementData,所以我上上面源代码里的判断是不是成立的,是不是为true,为true的话,我是不是可以进入判断体里,所以,明白了吧,这个判断就是判断你的容量是否为空,如果为空,我就把10传递给你,而这个10就是我下面要说的DEFAULT_CAPACITY
常量。
那好,进入,首先,你要明白Math.max()表示什么意思,它跟上面说到的Math.min()是相反的,这个是返回最大的那个,所以,不做过多解释,可以点进去看它的源码,很简单,我就不点了。
好,然后我们看看DEFAULT_CAPACITY,直译为默认容量,它是一个常量,值为10。而minCapacity是不是我们传进来的参数呀,size是不是为0,那么0+1就是1,很明显,1比10小,所以,最终返回的是10,重新赋值给minCapacity。最后带着minCapacity又传入了一个方法,注意这个方法将真正决定ArrayList是否真的要扩容,如下:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
modCount就是记录你实际修改集合的次数。
好,进行判断(注意,minCapacity是size+1,为什么size要加1,因为它要判断你这个数组如果添加进来一个元素,会不会导致数组容量不够,比如数组容量是10,而你集合的长度假设也是10,那么根据判断10-10等于0,0不大于0,所以为false,就不会去执行里面的grow方法,也就不会扩容,那么我加1测试一下,11-10不就大于0了吗,说明容量不够了,我要扩容,所以,明白了吗?),当前elementData.length为0,也就是容量为0,所以10-0就是10,10比0大,所以成立,进去,执行grow方法,注意这个grow方法就是真正来扩容的,也就是说,如果该判断成立,我们就扩容,如果不成立,我们就不扩容。而当前这个elementData是不是必须扩容呀,不然怎么添加元素进去(否则会报ArrayIndexOutOfBoundsException异常),所以让我们进入grow方法吧,如下:
private void grow(int minCapacity) {
// overflow-conscious code
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 is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
这个grow就是来真正的扩容了,我们平时不是会说ArrayList是一个动态数组吗,动态在哪?就在这个grow方法里,知道存不下了,我就自动扩容,这就是动态。
好,看上述源代码的第3行,很好说,oldCapacity就为0,好,执行第4行,这个第4行要注意了,等号右边的运算决定了你的数组要扩大到多少。然后看看oldCapacity >> 1
这个东西,这个我们就暂时理解成oldCapacity除以2,那么0除以2是不是还是0,0+0也为0,所以newCapacity就为0。但是我强调一遍,等号右边的运算其实等价于oldCapacity*1.5
,也就是说,它扩容,其实是扩容原数组的1.5倍,这个很重要,那么0*1.5是不是也一样是0呀,所以newCapacity为0是跑不掉的。继续向下执行,到第5行,0-10是不是一个负数,负数是不是小于0,成立,进去,那么newCapacity的值就不再是0了,而是10。然后再执行第7行判断,这个其实表示如果你当前的这个容量比数组的最大值还要大,那么它就会启动一个hugeCapacity来处理,这个不管。那么继续向下,是不是就到了第10行呀,Arrays.copyOf()这个函数上面说过,它是不是传了两个参数进去,等价替换为:elementData = Arrays.copyOf(elementData, 10);
,也就是说,它会把elementData当做原数组,然后把10传进去是因为它底层要创建一个容量为10的数组,接下来是不是要System.arraycopy进行复制呀,但是我原数组里一个元素也没有,所以赋值也没用,所以它就把一个容量为10的数组进行返回,重新赋值给elementData。
也就是说,当我们需要扩容的时候,就会创建一个新数组,新数组的容量是我们在grow()方法里计算好的,然后就会把原数组的所有数据一一复制进新数组,这就是扩容的流程。
所以,扩完之后,elementData的长度就为10了。也就是此时该数组索引为0到9,每个元素都为null。到elementData[size++] = e;
这一行,到这为止,因为elementData的容量不再为0了,既然不为0了,那么我们就可以往里添加元素了,注意在添加之前,size还为0,同时要注意它是后加加,这个很容易看懂,没啥好说的,然后就return true。
3.2 add(int index,E element)
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add(1,"aaa");
}
点击进入,如下:
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++;
}
然后进入rangeCheckForAdd方法,如下:
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
也就是说,它会判断我们传进来的索引有没有大于集合的长度,或者有没有小于0。很明显,index是1,size是0,所有成立,要抛异常,如下:
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:661)
at java.util.ArrayList.add(ArrayList.java:473)
at com.cht.ioc.TestIOC.main(TestIOC.java:9)
也就是说我们用这种方法在添加元素的时候要按索引顺序添加,不能跨索引添加,比如如下:
ArrayList<String> list = new ArrayList<String>();
list.add(0,"aaa");
list.add(1,"aaa");
list.add(3,"aaa");
1跟3之间是不是少了个2呀。不是连续的,要报错。也不能这么说,应该是你在添加的时候要顺序添加,除非你是插队的,比如如下:
ArrayList<String> list = new ArrayList<String>();
list.add(0,"aaa");
list.add(1,"bbb");
list.add(2,"ccc");
list.add(1,"ddd");
System.out.println(list);
这个就不会报错,因为你最后添加的虽然是索引1,但是它并不大于集合的长度呀,所以就不会报错,你把1换成4就不行啦,注意这个是插队,为什么是插队,因为你元素ddd要插入到bbb的前面。结果如下:
[aaa, ddd, bbb, ccc]
好,回归正题,那么我们把index换成0就ok了,如下:
ArrayList<String> list = new ArrayList<String>();
list.add(0,"aaa");
好,进入源代码,执行ensureCapacityInternal方法,这个方法其实跟上面讲的add(E e)是一样的,也是看看是否要扩容,要,就执行grow方法,不用,那就不用执行grow方法。很显然,是要扩容的,那么就会扩容一个容量为10的数组。然后下一个是System.arraycopy,说白了,你不是要在索引为0的位置添加一个元素吗,那么我就在原数组索引为0的位置开始往后后退1位,为你的新元素添加进去,说白了就是我前面说的插队。那么下一句elementData[index] = element;
就可以开始真正的赋值了。然后size++,没什么好说的。
3.3 addAll(Collection<? extends E> c)
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
ArrayList<String> list1 = new ArrayList<>();
list1.addAll(list);
System.out.println(list);
System.out.println(list1);
}
我们直接定位到第7行上,然后进去,如下:
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;
}
我们遇到了c.toArray(),一样,c就是list,此时这个list的size为3,是有元素的,然后我们调用toArray()方法,这个方法我们说过了,就是复制一份数组,跟list是一模一样的,只是容量不一样。再赋值给Object[] a。然后下一步,就是把a的长度赋值给numNew,也就是numNew为3。下一句,注意这里的size是新集合的长度,也就是0,0+3就是3,传入一个函数里,而该函数上面也见到过了,也是判断是否要扩容的。好,再下一句,System.arraycopy,原数组是不是a,是有值的啊,也就是list,目标数组是elementData,这个是空数组的,也就是list1,我们可以近似的把它看做这样的:System.arraycopy(list, 0, list1, 0, 3);
,从原数组的第0个索引开始复制3个到list1数组的索引为0的位置。那么此时此刻elementData就有值了。后面size+=numNew
比较简单,就不说了。
总之一句话,就是先把你原数组的长度和目标数组的长度加起来,然后把你加起来的长度看做是一个数组的长度,检验一下是否需要扩容,如果需要,就扩,不需要,就不扩,这样可以确保elementData的容量,为下一句System.arraycopy做准备,否则就会报数组越界异常,也就是ArrayIndexOutOfBoundsException,这样你在拷贝数据从a到elementData的时候就不用担心数组越界了,这点要注意。
3.4 addAll(int index,Collection<? extends E> c)
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
ArrayList<String> list1 = new ArrayList<String>();
list1.add("HAHA");
list1.add("haihai");
list1.addAll(1,list);
System.out.println(list);
System.out.println(list1);
}
让我们定位到第9行,然后进去,如下:
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;
}
第2行,不用说,是在校验索引index,上面说过了。看第4行,一样,将集合(list)转数组,然后赋值给a,那么a的长度就是3,也就是numNew为3,好,继续,下一行,一样,看看是否需要扩容,为System.arraycopy做好准备,不说了,跟上面一样。看第8行,size是list1的size,为2,index呢就是你传入的第一个参数的值,为1,那么2-1就是1,也就是numMoved为1,这个numMoved呢它表示要移动的元素的个数,说白了,你看下面System.arraycopy的最后一个参数就明白了,最后一个参数表示什么意思,它就是什么意思,没什么,可以自己私底下想一下,比如有10个元素,我要在索引为3的位置插入一个元素,那么原数组以索引为3的元素是不是要整体往后挪呀,那么总共有多少个元素在往后挪,根据它的算法size-index,也就是10-3,等于7,好,举起你的双手,数一下,是不是有7个元素在往后挪。
看第13行,注意上面的那个System.arraycopy是你list1上的自身移动,你list上的数据还没添加进来呢,所以是不是要把list上的数据添进来呀。System.arraycopy这个方法大家自己再好好感受下,可以画下图,可能比较难懂点,但想多了就不觉得难,其实很简单。
4. set方法
4.1 set(int index,E element)
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
String value = list.set(3,"dddd");
System.out.println("set方法返回值:"+value);
System.out.println("集合的元素:"+list);
}
让我们定位到第7行,然后进去,如下:
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
rangeCheck的话不妨进去看一下,如下:
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
也就是说,你传进来的的索引不能大于我的数组容量,否则抛数组越界异常,很简单,退出。看下一句,也就是第4行,进去,如下:
E elementData(int index) {
return (E) elementData[index];
}
这个也简单,就是把你原数组的那个值取出来。你看第6行,是不是reture出去了,这不用说吧,set()的返回值就是旧数据,这是基础知识。然后第5行是不是就是真正的更改值了呀,也非常简单。
5. get方法
5.1 get(int index)
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
String value = list.get(1);
System.out.println(value);
}
定位到第7行,进入源代码,如下:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
好了,没啥。
6. toString方法
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
String value = list.toString();
System.out.println(value);
定位到6行,点进去,如下:
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
注意注意,这个方法是在AbstractCollection这个抽象类中的,ArrayList是没有toString方法的,也就是说ArrayList是AbstractCollection的一个子类。第2行先不说,先看第3行,这个判断就是判断list集合是否有元素,有元素,那我们就不进入,好,跳到StringBuilder sb = new StringBuilder();
这里,不用说了,是用来拼接字符串的,好,继续向下,首先,先追加[
,这个不用说,然后进入for(;;)循环,其实是while循环,这应该都懂,进入,先把第一个数据取出来,赋值给e,再向下,看e是否恒等于this,this就是当前对象啦,比如如下:
后面的判断就不用说了,简单。
7. Iterator迭代器
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("java");
list.add("php");
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
定位到第5行,然后进去,如下:
public Iterator<E> iterator() {
return new Itr();
}
进入Itr类(ArrayList的内部类),如下:
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
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
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();
}
}
当我们去new上面这个类的时候,它会去初始化上面的三个成员变量,要注意。
cursor代表光标,或者叫指针,默认值为0,通过注释翻译为"要返回的下一个元素的索引"。lastRet 这个东西通过它的注释可以知道,它是返回最后一个元素的索引的,如果没有,则返回-1。expectedModCount = modCount;
是不是将集合实际修改次数赋值给预期修改次数。
好,让我们走到while(iterator.hasNext()){
这里吧,进去,如下:
public boolean hasNext() {
return cursor != size;
}
它呢是判断光标是否不等于集合的size,cursor是0,size是2,0!=2,成立,说明集合有元素,返回true。开始执行iterator.next()
代码,进入next,如下:
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
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];
}
看checkForComodification方法里的判断,它是判断预期修改集合次数是否和实际修改集合次数一样,进去看一下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
答案是一样的,刚刚才赋好的值嘛。那么为什么要判断一下,你看它里面所抛的异常,翻译为并发修改异常,也就是说在多线程环境下可能出现的异常,什么意思?也就是说,当我们在遍历集合的时候,突然有另一个线程调用了remove()方法删除了集合中的某一个元素,那么就可能会造成modCount和expectedModCount不相等,注意是可能,后面会分析。
继续往下看,这时i就等于0,继续往下,看i是否大于等于集合的size,如果大于或等于说明没有元素了,很明显不大,继续往下,走到Object[] elementData
这一行,ArrayList.this就相当于list这个集合,因为是要调用外部类中的elementData,所以不能直接this。然后把这个数组的地址赋值给elementData这个变量,再进入下一个判断if (i >= elementData.length)
,这个也是针对多线程环境下并发修改的。然后光标,就要向下移动了,看,是不是加1了,然后从数组中取出元素返回出去。
7.1 并发修改异常产生的原因分析
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add("java");
list.add("php");
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Object o = iterator.next();
if(o.equals("php")){
list.remove("php");
}
}
System.out.println(list);
}
结果如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at thread.Test.main(Test.java:12)
其实根据它的错误提示,我们可以定位到某一行,比如第2行,在ArrayList类的第909行,也就是checkForComodification方法。我们先一步步分析,首先,我们先点位到第6行,进去,也就是如下:
public boolean hasNext() {
return cursor != size;
}
size代表集合的长度,为2,好说。看cursor,因为是刚刚初始化,所以为0,那么0不等于2,为true,那么就把true返回,成功进入while方法体,开始执行next()方法,如下:
public E next() {
checkForComodification();
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
先判断modCount跟expectedModCount是否相等,因为数组并没有添加元素啥的,还是原来的2(add的时候modCount会加加),所以modCount并不会加加,所以是一样的,相等的,所以判断不成立,那么就不会抛出错误啦。cursor为0,前面说过,然后赋值给i,然后判断i是否大于等于数组长度,是为了防止没有该索引,比如,如下:
public static void main(String[] args){
ArrayList list = new ArrayList();
Iterator iterator = list.iterator();
System.out.println(iterator.next());
}
像上面这段代码,就会抛出NoSuchElementException。
让我们继续,elementData就不说了,下一句,判断i >= elementData.length是否成立,目前是不成立的,所以没进去,继续往下走,就是cursor加1了,那么就意味着指针也向下走了,所以安全返回。
安全返回后是不是赋值给o,然后是不是判断o是否等于php,很明显,是不等于的,所以不进去。那么继续while循环,还是先执行hasNext(),1并不等于2,所以成立,进入while方法体,开始执行next()方法,modCount还是等于expectedModCount,还是那句话,集合并没有发生任何改变。然后i为1,cursor变为2,这些我就一笔带过,好,现在重点来了,要来执行list.remove("php");
语句了,那么,我们就点进remove方法吧,如下:
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;
}
这个就很简单了,直接看else吧,是不是要遍历集合呀,看看你传入的php在我集合里有没有,如果有,我就进去调用fastRemove方法,所以,让我们点进fastRemove看看吧,如下:
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
}
相信到这,你就明白了,为什么会抛出并发修改异常,因为你modCount加加了,那么,你下次再去next的时候,它发现你modCount和expectedModCount不一致,那么就真正的抛出ConcurrentModificationException异常了,所以,明白了吗?下面的就是把我们要删除的数据置为null。
好,让我们继续,它又一次执行hasNext,注意cursor为2,那么size呢,因为我们删除了一个,所以为1,2并不等于1,所以成立,到执行next()中的checkForComodification方法就报错了。
7.2 并发修改异常的特殊情况
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add("php");
list.add("java");
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Object o = iterator.next();
if(o.equals("php")){
list.remove("php");
}
}
System.out.println(list);
}
注意,这里我把添加顺序交换了,原本php是在java的后面的,现在它在java的前面,就这点改变,其它不变,然后看看运行结果,如下:
奇怪,这是为什么呢?那么我们就从hasNext开始点进去看看吧,如下:
public boolean hasNext() {
return cursor != size;
}
cursor为0,size为2,所以为true,进去while方法体,开始执行next()方法,这个就不多说了,然后remove,那么modCount就会加加,好,再次hasNext,cursor为多少?因为你刚刚执行了一次next(),所以cursor就为1,size因为你刚删除了1个,所以2-1为1,所以结果为false,为false,就不进去啰,那么就会退出while循环,根本就接触不到next()方法中的checkForComodification方法。
结论
当要删除的元素在集合的倒数第二个位置的时候,不会产生并发修改异常,原因是因为在调用hasNext方法的时候光标的值和集合的长度一样,那么就会返回false,因此就不会再去调用next方法获取集合的元素,因为没有去调,所以底层就不会产生并发修改异常。
7.3 Iterator中默认的remove()方法
注意,不是ArrayList中的remove方法,而是Iterator类给我们提供的remove方法,也就是说,用它自身给我们提供的remove方法,就可以避免上述的并发修改异常,是不是这样呢,可以测试一下,如下:
注意哈,我上述添加的顺序交换过来了,也就是php在后,也就是7.1节的那种情况。好,看结果,是不是并没有报酬,输出的list中php是不是删了,那么它的底层到底是怎么做的呢?如下:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
lastRet是不小于0的,所以不会抛IllegalStateException异常,checkForComodification就不说了,都知道,反正不会抛异常就是了,继续向下,注意,ArrayList.this.remove调的是ArrayList的方法,不是Iterator里remove的方法。然后注意lastRet的值为1,因为我上面经历了两次next(),是不是每次return,lastRet总会被赋值呀,好,那么它就会把1当成索引传进去,因为它是根据索引删除的,那么既然是根据ArrayList里的remove方法删除的,那么它的modCount就会加加。继续,是不是就把lastRet赋值给cursor,那么cursor原先是2,现在就为1。继续向下,lastRet又变为-1了,为什么变为-1?因为你是不是有可能调了两次Iterator里的remove方法,你看源代码,它一进来是不是叫要判断你的lastRet是否小于0,所以,明白了吗,就是防止你重复删除。好,下一句,就把modCount赋给expectedModCount,这是最最最关键的代码了,因为这样modCount的值跟expectedModCount不就相等了吗?
一句话概括:其实Iterator里的remove方法底层调的还是ArrayList的remove()方法,只是它把实际修改的次数赋值给了预期修改的次数,所以导致两者相等。
8. remove()
这里我要说的是ArrayList里的remove方法,我们就把对应的源代码拿过来吧,如下:
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
}
假设size等于6,index等于2,elementData是[1,2,3,4,5,6]。
OK,根据上面提供的值,我们来仔细分析它是怎么移位的,同时有什么缺点。首先,numMoved为3,没问题,好,进入执行System.arraycopy,等价替换为:System.arraycopy(elementData, 3, elementData, 2, 3);
,也就是说,在elementData的索引为3的数组上开始复制,复制3个,到自身索引为2的位置覆盖,说白了,是不是从索引为3的往后开始向前移动1位呀,这样是不是就把索引为2的值给覆盖了,这样就达到删除的效果啦。但是这样删除有什么问题呢?如下:
public static void main(String[] args) {
ArrayList arrayList=new ArrayList();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
System.out.println(arrayList.toString());
for (int i=0;i<arrayList.size();i++){
arrayList.remove(i);
}
System.out.println(arrayList.toString());
}
结果如下:
[1, 2, 3]
[2]
根本删不干净呀,这是怎么回事呢?我们画图分析一下:
那么如何解决?应该是从后往前删,如下代码:
public static void main(String[] args) {
ArrayList arrayList=new ArrayList();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
System.out.println(arrayList.toString());
for (int i=(arrayList.size()-1);i>=0;i--){
arrayList.remove(i);
}
System.out.println(arrayList.toString());
}
自己琢磨。。
9. clear()
ArrayList list = new ArrayList();
list.add("java");
list.add("php");
System.out.println(list);
list.clear();
System.out.println(list);
效果如下:
[java, php]
[]
进入源码,如下:
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
没什么好说的,通过for循环把每个元素的值置位null,最后size为0。
10. contains(Object o)
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add("java");
list.add("php");
if(!list.contains("C#")){
list.add("C#");
}
System.out.println(list);
}
进入源代码,如下:
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
再进入indexOf,如下:
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;
}
只看else,因为我们传进来的不是空,好,我们是不是要进行for循环遍历呀,找到有没有一个东西叫C#的,很遗憾,没有,没有就退出if判断,直接return一个-1到上一级,那么-1大于等于0吗?是不是为false,那好,返回false,有返回上一级,是不是取反,取反就变为真了,为真,就进入执行add("C#");
操作,这个就不用说了。
11. isEmpty
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add("java");
list.add("php");
boolean empty = list.isEmpty();
System.out.println(empty);
}
结果为false。好,查看源码,如下:
public boolean isEmpty() {
return size == 0;
}
不用说了,贼简单。
12. debug案例演示
总结:
- ArrayList中维护了一个Object类型的数组elementData,也就是它把数据放到elementData数组里面,它的底层是数组。
- 当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
- 如果使用的是指定大小的构造器(比如new ArrayList(8),表示指定大小为8位的长度),则初始化elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CollectionMethod {
@SuppressWarnings({"all"})
public static void main(String[] args) {
ArrayList list1 = new ArrayList();
for (int i=1;i<=10;i++)
{
list1.add(i);
}
for (int i=11;i<=15;i++)
{
list1.add(i);
}
list1.add(100);
list1.add(200);
list1.add(null);
}
}
开始分析,如下:
然后点击蓝色向下箭头Step Into,进入如下界面:
先说一下怎么用debug,主要说两个,一个是Step into,一个是Step over。Step into遇到子函数就会进入该函数执行,比如上面我new了一下ArrayList(),是不是就进入ArayList()的这个构造函数呀。Step over呢是不会进入子函数里的,但是子函数是会执行的,只是单纯的没进入而已,就往下走了。说白了,就是一个遇到函数就会进去执行再出来,一个就算遇到也不会进去,而是继续往下,一条条顺序执行。
如果说你Step Into无效,也就是不能进入以上界面,那么就按照如下操作就好了,如下图所示:
好,继续,点击Step over,如下:
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
说过了,但当做复习,进去一下吧,如下:
结论:当我们用无参构造器去初始化ArrayList的时候,默认的数组长度就为0。注意,不是一上来就初始化10,而是添加。
然后继续向下(Step into),就会回到原处,再向下就进入for循环,如下:
现在要来添加元素了,但注意,因为是数值型的,所以它要进行一下包装,包装成Integer类型,我们可以Step into进入,如下:
一直点Step into,就会进入add方法,如下:
分析一下ensureCapacityInternal,我们进入,如下:
不说了,继续:
10跟1比,谁大,是不是10大呀,那么minCapacity是不是就是10,然后继续向下,又会进入一个方法,然后把10传进去,来确定是不是真的要扩容,我们进入该方法,如下:
继续:
扩完之后,elementData的长度就为10了。也就是此时该数组索引为0到9,每个元素都为null。那么elementData既然是10了,下一次再进入该方法的时候是不是就是以1.5倍的方式扩容呢?不妨自己试一试。结果当然是的。
然后一直按Step over返回,到如下界面:
现在elementData就有空间了,原先是空的,现在就不是空的了,不是空的就可以添加数据进去了,注意此时的size还是0,那么就把数据真正的加入到elementData里去了。往下执行一步,如下:
再返回,就到头了。其它以此类推。然后我们进入下一个for循环,如下:
注意,经过上次for循环,10个位置已经填满了,那么这一次就要扩容了,并且是按1.5倍扩容。后面就自己去调了,我懒,就不把图一个个截下来了。下一句,如下:
经过上一次的15次循环,又满了,又需要扩容了,扩容多少个,那么就看还没添进去元素之前的数组长度为多少,是不是15,那么下一次的扩容的长度是不是15+(15/2)呀。最后注意,我们在Debugger的时候,为了减少某些不必要的麻烦,我们要做如下操作:
13. 补充
13.1 如何复制某个ArrayList到另一个ArrayList中去?
- 使用clone()方法。
- 使用ArrayList构造方法。
- 使用addAll方法。
13.2 ArrayList和LinkedList的区别
- 数据结构不一样,ArrayList底层是数组,LinkedList底层是双向链表。
- ArrayList因为实现了RandomAccess标记接口,所以ArrayList支持随机访问,而LinkedList不支持。
- 对于频繁读取集合中的元素,推荐使用ArrayList,而对于频繁的插入和删除,推荐使用LinkedList。