长文警告!!笔者呕心沥血之作,希望能帮到你
文章目录
- 继承关系
- 类中的属性
- 构造函数
- 重头戏 boolean add(E e)
- 确保容量足够(达到需要的最小值)void ensureCapacityInternal(int minCapacity)
- 提升容量 void grow(int minCapacity)
- 情况一:使用无参构造,第一次add(E e):
- 情况二:newCapacity没超过MAX_ARRAY_SIZE,正常扩容1.5倍
- 情况三:newCapacity超过MAX_ARRAY_SIZE,扩容至MAX_ARRAY_SIZE
- 其他add方法
- add总结
- 删除元素
- remove(int index)删除指定位置的元素
- remove(Object o)删除指定的对象
- boolean removeAll(Collection<?> c)删除当前list和传入参数c的"交集"
- 其他方法
继承关系
实现的接口
- List接口:我们会出现这样一个疑问,在查看了ArrayList的父类AbstractList都实现了List接口,而ArrayList又是AbstractList的子类,那为什么子类ArrayList还是去实现一遍呢?有的人说是为了查看代码方便,使观看者一目了然,说法不一,但每一个让我感觉合理的,但是在stackOverFlow中找到了答案,这里其实很有趣。开发这个collection 的作者Josh说:这其实是一个mistake,因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。原文出处
- RandomAccess接口:这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如arrayList。
而没有实现该接口的话,使用Iterator来迭代性能更高,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。 - Cloneable接口:实现了该接口,就可以使用Object.Clone()方法了。
- Serializable接口:实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类。
类中的属性
值得注意的是,ArrayList 底层用于存储元素的Object[] elementData
是声明为transient的
这么做的的目的是在进行序列化时,并不会把整个数组序列化,而是只把size
大小的元素序列化,会把为null的部分舍去。这个做的好处是减少序列化的元素,提升效率和节省空间。另外,IO操作是费时的。一般情况下,elementData
默认容量是10,在增加元素时如果容量不够,扩容会提升1.5倍,所以elementData
中有可能会有大量的空位出现。
构造函数
无参构造ArrayList()
可以看到注释里面说,初始化的是一个容量为 10 的空 list ,但是之前就已经提到了this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
中的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是个空的对象数组。那又何来容量为10呢?
再看这两个变量,一切就说得通了:
原来this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
就是一个标记,标记初始化ArrayList时使用的是无参构造,至于elementData
容量变成10则推迟到添加第一个元素时才执行。
这么做的好处是显而易见的,如果我们只是无参构造一个ArrayList而没有使用他,那么底层的elementData
只是Object[] elementData= {};
这样能节省空间和操作的步骤,提升效率。类似的思想就像是单例模式的懒汉式和饿汉式
指定容量 ArrayList(int initialCapacity)
这个很简单,注释的翻译笔者已经用红字写在图里
很少用到的ArrayList(Collection<? extends E> c)
可能有人会有疑问按顺序返回是怎么回事,笔者做了以下实验:
Stack按顺序返回的是123,
使用pop返回的是321,
重头戏 boolean add(E e)
表面上看,只需要确保动态数组elementData装得下
然后把他用elementData[size++] = e
装下就可以了
但是其实要确保装得下并不是很容易的事情
确保容量足够(达到需要的最小值)void ensureCapacityInternal(int minCapacity)
这里minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
为什么要取最小值呢?
原来当以使用无参构造之后用addAll(c)
的时候可能传入的参数就会大于10,最终elementData.length
小于size + numNew
的话就容量就不够了。
提升容量 void grow(int minCapacity)
容量提升默认是1.5倍,但并非绝对着这样
情况一:使用无参构造,第一次add(E e):
笔者在Debug模式下观测得到,这种情况下,elementData
最终倍初始化为长度为10的对象数组Object[10]
情况二:newCapacity没超过MAX_ARRAY_SIZE,正常扩容1.5倍
可以看到使用了数组复制,如果频繁扩容会效率不高,尤其是数据量比较大时。但是数据量比较大时,一次扩容也会比较大,并不容易频繁填满导致频繁扩容
情况三:newCapacity超过MAX_ARRAY_SIZE,扩容至MAX_ARRAY_SIZE
其中MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
,如果需要,所以还可以再次扩容:
自此,再次扩容就会抛出OutOfMemoryError
错误了,因为Integer.MAX_VALUE + 1
= -1,不是我瞎说,代码为证:
具体为什么是这样的呢?笔者又打开了Integer
的源码进行了分析:
0x7fffffff + 1
=0x80000000
说深入了就跟计算机表示整数使用的是补码有关,想了解的同学可以去学习一下补码
其他add方法
以add(int index, E element)
为例,非常地朴实无华且枯燥↓
其他add方法都比较简单,不再赘述
add总结
如果使用new ArrayList();
,elementData
初始为null,在第一次add时才初始化。
如果用循环add(e)
添加元素,每次需要扩容时,通过位运算实现容量扩容1.5倍,因为是位运算,所以15扩容后是22。
如果使用addAll(c)
的时候,有可能扩容1.5倍也不够,那么会直接扩容到刚好能装下的容量。
删除元素
remove(int index)删除指定位置的元素
可能有部分读者对
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
这一句有疑问,笔者翻月了System
的源码发现
显然,这行代码的意思就是把原来index + 1
到末尾的元素复制到index
到原来末尾-1的位置。
至于rangeCheckForAdd(int index)
,不是瞎子应该都能读懂↓
remove(Object o)删除指定的对象
fastRemove(int index)
和remove(int index)
的区别只是:
- 私有
- 没有返回值
- 不要判断index是否超出
size
boolean removeAll(Collection<?> c)删除当前list和传入参数c的"交集"
这个方法会删除arrsyList
中所有在c
中出现过的元素
batchRemove(Collection<?> c, boolean complement) 真正删除元素
此处的疑问主要有二:
- 为什么
final Object[] elementData = this.elementData;
会声明为 final 类型 - 调用
AbstractCollection
中的contains(Object o)
会抛出异常吗?
解答:
第一个问题:
首先,Object[] elementData
是一个引用类型变量,这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变的。也就是elementData[1]
指向的对象可以改变。所以后续的一系列操作都是没有问题的。
其次,定义为 final 类型之后不会在后续的操作中发生操作了其他的 elementData
里面的数据的情况,保障操作的都是this.elementData
里面的数据。
第二个问题:
在AbstractCollection
中找到contains(Object o)
函数并不会抛出异常!
但是有可能会因为别的原因发生异常,注释里面说的是“保持与AbstractCollection
的行为兼容性”,在我看来,应该是一种保险的写法。
其他方法
剩余的方法比较朴实无华且枯燥,直接看图: