文章目录
- 一、继承关系
- 二、源码解读
- 2.1 构造函数
- 2.2 常用方法
- 2.2.1 public boolean add(E e)
- 2.2.2 public void add(int index, E element)
- 2.2.3 public E get(int index)
- 2.2.4 public int size(),public boolean isEmpty(),public void clear()
- 2.2.5 public int indexOf(Object o)
- 2.2.6 public boolean contains(Object o)
- 2.2.7 public int lastIndexOf(Object o)
- 2.2.8 public E set(int index, E element)
- 2.2.9 public E remove(int index)
- 2.2.10 public boolean remove(Object o)
- 2.2.11 public boolean addAll(Collection<? extends E> c)
- 三、其他
- 3.1 关于for循环中remove
- 3.2 关于fast-fail
JDK版本: 1.8
IDEA版本:2020.01
正片开始
一、继承关系
顶层父类是Iterable类,
二、源码解读
2.1 构造函数
2.1.1 无参构造函数:ArrayList()
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_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 = {};
然后就没了。可以看到ArrayList的无参构造函数都没有去管elementData的长度需要多少直接设置为一个空数组(也就是长度为0的数组),所以说在调用无参构造函数初始化一个ArrayList的时候
ArrayList<String> list = new ArrayList();
初始化数组长度为0,不要听风是雨觉得ArrayList的初始化长度是10,应该要指出在无参构造后第一次进行add操作时才初始化数组长度为10。
2.1.2 ArrayList(int 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
*/
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);
}
}
不多说这里的逻辑了,就是很疑惑这里为啥又来一个EMPTY_ELEMENTDATA,我看它的值跟DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一样一样的。看了下DEFAULTCAPACITY_EMPTY_ELEMENTDATA的注释,大概意思是在第一次add(E e)时用来区分是否要把容量初始化为默认的10(用的是这个私有常量:DEFAULT_CAPACITY)。
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
2.1.3 ArrayList(Collection<? extends E> c)
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();把传进来的集合转化为数组并赋值给了elementData
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
这里的if判断我也有点疑惑,前面的源码注释好像告诉我没加这个if前这可能会引发bug
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;设置为{}
}
}
2.2 常用方法
2.2.1 public boolean add(E e)
public boolean add(E e) {
确保容量够,不够则扩容
ensureCapacityInternal(size + 1); // Increments modCount!!这里用注释提醒在ensureCapacityInternal方法里modCount会增加,modCount是用来判断是否fast-fail
elementData[size++] = e;赋值并size+1
return true;
}
保证容量的方法(内部使用)
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
如果是默认的空数组则取minCapacity和默认值10的最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);“我可能要开始扩容了”
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;终于见到modCount了,这玩意是给fast-fail机制用的
// overflow-conscious code哦吼,具有溢出意识的code
if (minCapacity - elementData.length > 0)大于数组容量,就扩容呗
grow(minCapacity);
}
真正进行扩容的地方
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;保存旧的容量
int newCapacity = oldCapacity + (oldCapacity >> 1);新容量等于1.5倍旧容量(或者等于1.5倍旧容量再减一,因为奇数除以2会丢失1)
if (newCapacity - minCapacity < 0)计算出新容量是不是还小于需要的容量
newCapacity = minCapacity;是的话木有办法了只能用需要的容量了
if (newCapacity - MAX_ARRAY_SIZE > 0)新容量已经大于MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8
newCapacity = hugeCapacity(minCapacity);计算大容量,结果有三种,聊到再说
// minCapacity is usually close to size, so this is a win:哈哈,源码作者觉得代码大部分情况下minCapacity应该是要跟size接近的
elementData = Arrays.copyOf(elementData, newCapacity);根据新容量拷贝一个数组并赋值给elementData
}
2.2.2 public void add(int index, E element)
public void add(int index, E element) {
rangeCheckForAdd(index);检查是否小于0或者大于size,说明index可以等于size(这时候其实就跟直接add(E e)差不多了)
ensureCapacityInternal(size + 1); // Increments modCount!!确保容量够,这个方法上面的add(E e)有出现过,这里不再说明
System.arraycopy(elementData, index, elementData, index + 1, size - index);index以及后面的元素全部往后移一位
elementData[index] = element;将添加的元素放置在index的位置
size++;size+1
}
这里插播一条杂谈:
ArrayList使用数组实现,数组容量实际上不能真正的动态扩展,因为数组本质上是一连串连续的地址,而我们感受到ArrayList的扩容能力其实是开辟了一个更大容量的数组,并把旧数组的内容依次赋值到新数组上,最后把原来的数组引用(指针)指向新数组。所以很多时候都会用到
Arrays.copyOf(T[] original, int newLength)
或Arrays.copyOf(U[] original, int newLength, Class<? extends T[]> newType)
和java System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
(其实无论是Arrays.copyOf(T[] original, int newLength)
或Arrays.copyOf(U[] original, int newLength, Class<? extends T[]> newType)
中最终也是用到了System.arraycopy这个native方法)
2.2.3 public E get(int index)
好嘛~就两行,不过想想也确实,毕竟是数组,根据index进行定位那是O(1)的时间/空间复杂度啊
public E get(int index) {
rangeCheck(index);范围检查
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)大于size的抛IndexOutOfBoundsException异常,小于0的这里没有去管
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];取出对应位置后的元素根据泛型强转,这里index<0也会抛IndexOutOfBoundsException异常
}
2.2.4 public int size(),public boolean isEmpty(),public void clear()
这三个的源码比较简单直观,一起吧
public int size() {
return size;二话不说,直接返回即可
}
public boolean isEmpty() {
return size == 0;size为0则为empty
}
public void clear() {
modCount++;修改数组元素的操作都会有这玩意
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;全部置为空,让原来的元素不被引用,等待这些元素的是GC
size = 0;重置size为0
}
2.2.5 public int indexOf(Object o)
这个方法是找出第一个与o相同元素的index。
public int indexOf(Object o) {
if (o == null) {
如果o为null遍历时直接用==null判断元素
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
如果o不为null就用equals了
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;找不到就返回-1
}
2.2.6 public boolean contains(Object o)
public boolean contains(Object o) {
直接复用indexOf查找的结果,如果查找的结果大于等于0那就是包含该元素咯
return indexOf(o) >= 0;
}
有的疑惑为啥要根据o是否为null在用代码中用了两个for循环,这不是很冗杂吗,直接这样它不香吗?
public int indexOf(Object o) {
for (int i = 0; i < size; i++)
if (o == null ? elementData[i]==null : o.equals(elementData[i]))
return i;
return -1;
}
我还就真的测了一下,发现在千万级别的差别真心不大,更别说大多数场景下的使用了。
2.2.7 public int lastIndexOf(Object o)
这个跟indexOf差不多,只不过换成了反向查找
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.2.8 public E set(int index, E element)
替换index位置的元素
public E set(int index, E element) {
rangeCheck(index);范围检查
E oldValue = elementData(index);保存旧值
elementData[index] = element;替换成新值
return oldValue;返回旧值
}
2.2.9 public E remove(int index)
去除index位置的元素
public E remove(int index) {
rangeCheck(index);范围检查
modCount++;
E oldValue = elementData(index);保存旧值
把index位置后的元素往前移一位,这里在计算要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
/*
前移一位但其实是index位置后的位置往前复制了,复制后最后的元素会在结尾连续出现两份,
所以下一步将最后一个元素置null。
举个例子:假设elementData = [0, 1, 2],要remove的index为1,那么经过这一步后,
elementData = [0, 2, 2]
*/
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work经过这一步后,elementData = [0, 2, 2]将变成elementData = [0, 2, null]
return oldValue;返回旧值
}
2.2.10 public boolean remove(Object o)
for循环查找,然后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;
fastRemove的逻辑跟public E remove(int index)
里的差不多。。
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
}
2.2.11 public boolean addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();先把传来的集合转化为Object数组
int numNew = a.length;确定新增的个数
ensureCapacityInternal(size + numNew); // Increments modCount确保容量够
System.arraycopy(a, 0, elementData, size, numNew);从数组a的0位置开始复制numNew个元素到elementData(从size位置开始)
size += numNew;调整size
return numNew != 0;
}
三、其他
3.1 关于for循环中remove
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(0, 1, 2, 3));
System.out.println("before removing, list = " + list);//①
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println("after removing, list = " + list);//②
第一眼看去似乎①应该输出before removing, list = [0, 1, 2, 3]
,且②应该输出after removing, list = []
。
然而②的结果是after removing, list = [1, 3]
,导致这个结果的原因是i
在不停增长,而size
在不停减小(元素
在elementData
中不断左移),过程大致是是这样的:
i = 0,list.size() = 3;list = [0, 1, 2, 3]
经过remove(i)即remove(0)后,list = [1, 2, 3],i++ ——> i = 1
i = 1,list.size() = 2;list = [1, 2, 3]
经过remove(i)即remove(1)后,list = [1, 3],i++ ——> i = 2
然后发现i = 2 已经 不满足 i < list.size()=2的条件了,便结束循环,所以结果就是list = [1, 3]。
所以当你写到类似下面的这段逻辑代码时:
for (int i = 0; i < list.size(); i++) {
Integer a = list.get(i);
if (a > 1) {//match condition then remove the element
remove(i);
}
}
只要对list进行反向遍历,就没有上面正向遍历的问题了:
for (int i = list.size() - 1; i >= 0; i--) {
Integer a = list.get(i);
if (a > 1) {//match condition then remove the element
remove(i);
}
}
3.2 关于fast-fail
单线程情况下关于骚操作导致的ConcurrentModificationException
,如下:
import java.util.ArrayList;
import java.util.Arrays;
/**
* @author NOknow
* @version 1.0
* @date 2020/09/05
*/
public class FastFail {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
list.remove(s);
System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));
}
}
}
控制台结果:
'a' has been removed, current list is:[b, c, d]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.jw.collection.list.arraylist.FastFail.main(FastFail.java:16)
what?为啥只有元素a被移除了?为啥报错的是第16行?
要回答这两个问题就要去看对应的class文件了。
地球人都知道编译器把foreach循环替换成了迭代器了,class文件样貌如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
public class FastFail {
public FastFail() {
}
public static void main(String[] args) {
ArrayList<String> list = new ArrayList(Arrays.asList("a", "b", "c", "d"));
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
list.remove(s);
System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));
}
}
}
于是再跟进list.iterator()
,发现他返回了一个ArrayList的内部的一个迭代器:
public Iterator<E> iterator() {
return new Itr();
}
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
int expectedModCount = modCount;//这里是个重点
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];
}
final void checkForComodification() {
if (modCount != expectedModCount)//好了,恭喜你发现了最终奥秘
throw new ConcurrentModificationException();
}
/*此处省略了一些其他源代码*/
}
整个过程就是:
1、list.iterator();
返回一个迭代器,迭代器中的expectedModCount
的值来自list,这时候迭代器的expectedModCount
与list的modCount
相等,都等于0(不要问我为什么等于0)。
2、代码来到while(var2.hasNext())
,很正常,进入while循环里。
3、代码来到String s = (String)var2.next();
,很明显此时还是:迭代器的expectedModCount
与list的modCount
相等,都等于0。
4、代码来到list.remove(s);
,关键点来了,从上面的源码分析可以知道,remove里对list的modCount
进行了自增,此时:迭代器的expectedModCount=0
,But !!!但是!!!, list的modCount=1
。此时remove操作还是很正常地进行下去的,好,继续。
5、代码来到System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));
,没毛病。
6、新的循环开始:代码来到while(var2.hasNext())
,没毛病,继续。
7、代码来到String s = (String)var2.next();
,好了,报错了,就是因为迭代器的next()函数的第一行代码:checkForComodification();
,此时:迭代器的expectedModCount=0
!= list的modCount=1
。
是不是才反应过来:“哦,原来你用list的迭代器去遍历元素,然后又用list的remove操作去除元素,应该直接用迭代器本身去remove的呀。”——对咯,就是这样:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
iterator.remove();
System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));
}
}
控制台打印结果:
'a' has been removed, current list is:[b, c, d]
'b' has been removed, current list is:[c, d]
'c' has been removed, current list is:[d]
'd' has been removed, current list is:[]
快~蹭着热乎,去把迭代器的remove的原理搞清楚。(还是去把ArrayList的迭代器的实现都看看吧)
多线程情况下导致的fast-fail的原因本质跟上面是一样一样的,人家ArrayList初心就是给单线程用的,不然也不会导致Vector失宠。
jend.