JDK源码之ArrayList
下文带/**/为源码注释//为个人注释。源代码使用这个颜色
无参数构造器初始化做了什么?
add()添加元素做了什么?
ArrayList如何扩容?
使用有参构造的必要性!
get(int index)可能抛出两个异常!
set(dex,ele)就是替换数组中对应下标的元素!
remove(int index),巧妙使用arraycopy(.....)一个本地方法
remove(Object o),删除第一个匹配的元素。
三种迭代删除的方式。
clear()清空元素,让GC释放元素内存
无参数构造器初始化做了什么?
//无参构造函数
public ArrayList() {
//无参构造函数将一个常量复制给了一个成员变量。
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/*
存储ArrayList元素的数组缓冲区。ArrayList的容量是这个数组缓冲区的长度。
任何带有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList。
将在添加第一个元素时扩展为DEFAULT_CAPACITY。
*/
//这是一个没有初始化的数组,可存储类型是Object这意味着集合内可以存储任何的元素,它才是最终存储元素的地方。
//ArrayList的容量是这个数组的长度,也就是说最终是这个数组存放元素,这里有个问题,这个数组的长度是元素的总
//个数吗?如果数组长度是100,能证明集合的元素有100个吗?显然不是的,这个数组如果长度是10,集合元素的总个数
//可能只有5个。当使用无参构造创建集合时这个数组就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在添加第一个
//元素后它的长度将变成 DEFAULT_CAPACITY 意味这这个值是默认值,ArrayList集合的默认长度,也是该数组的长度。
transient Object[] elementData;
/*
用于默认大小的空实例的共享空数组实例。
我们将其与EMPTY_ELEMENTDATA区分开来,以便知道添加第一个元素时需要膨胀多少。
*/
//这个翻译很不标准,但是能看出来使用无参构造时 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/*
默认初始容量。
*/
//在添加第一个元素时 elementData扩展为DEFAULT_CAPACITY可默认的数组长度是10
private static final int DEFAULT_CAPACITY = 10;
/*
用于空实例的共享空数组实例。
*/
//目前还不知道这个的作用,盲猜是一个临时容器
private static final Object[] EMPTY_ELEMENTDATA = {};
add()添加元素做了什么?
/*
将指定的元素追加到列表的末尾。
*/
//添加单个元素,按照上边的注释第一次添加数组的长度就会变成10,问题是
//数组的长度一旦确定无法改变。上边已经赋值为{}长度为0,它是怎么改变的?
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
/*
ArrayList的大小(包含的元素数量)。
*/
//注释来看它表示集合元素的数量。默认没有赋值。作为成员变量没有赋值默认是0
private int size;
//调用它的是add(E e)方法中的ensureCapacityInternal(size + 1);
//传入的值就是1
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//参数是{}和1,看上边elementData被初始化的就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA也就是{}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//第一次添加这个判断是成立的,返回值是Math.max(DEFAULT_CAPACITY, minCapacity);
//DEFAULT_CAPACITY是10,minCapacity是1.Math.max(a,b);返回比较大的值,也就是10.
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//入参是10
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//当前这个判断是成立的。因为elementData还是{}
if (minCapacity - elementData.length > 0)
//入参是10
grow(minCapacity);
}
/*
这个列表在结构上被修改的次数。结构修改是指那些改变列表大小的修改,或者以某种方式扰乱列表,使得迭代过程可能产生不正确的结果。
*/
//从字面意思看,每次修改列表(列表指的是集合还是数组?)都会记录次数。
protected transient int modCount = 0;
/*
增加容量,以确保它至少可以容纳最小容量参数指定的元素数量。
*/
//入参是10
private void grow(int minCapacity) {
//目前这个值是0因为elementData是{}
int oldCapacity = elementData.length;
//这个搞不懂啊,0右移一位不还是0?(0/1/2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//0-10<0成立走这个newCapacity=10
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//0-MAX_ARRAY_SIZE (这个值在下边介绍为2147483639)>0。在这里是不成立的。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//入参是{}和10.Arrays.copyOf(a,b);表示传入一个数组和容量,并返回一个新数组
//新的数组会拷贝老数组的元素,如果新数组的长度比老数组的长度多则剩下的数组元素补T。如果新数组的长度和老数组
//的长度相同则只拷贝,如果新数组长度没有老数组多,则能装几个装几个。详细的看下图运行结果。
//到现在elementData终于不是{},而是{0,0,0,0,0,0,0,0,0,0}长度为10的数组。注意其实数组中存的是T也就是你原来数组
//中元素是什么类型对应的默认值就是什么类型如果原来数组存的int那就是0,如果是String就是null
elementData = Arrays.copyOf(elementData, newCapacity);
}
/*
要分配的数组的最大大小。一些虚拟机在数组中保留一些头字。尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超过VM限制
*/
//int的最大值-8=2147483639
//这里得到一个有用信息,List中的数组最大值是2147483639也就是说这个List集合的大小是不会无限扩大的。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
整理当前得到的线索,ListArrayList使用elementData数组存放集合元素,如果是无参构造实例化集合则该数组默认是{}
当第一次add(T)则会生成一个新的数组,新数组的长度是10,该数组内全部都是默认值,新数组的引用将会给elementData。数组的长度最大为int
最大值-8,并不是无线膨胀的。size代表当前集合内有多少个元素,lementData[size++] = e;最后将要添加的e赋值到elementDatae[0]位置处。
第一个元素添加成功。
ArrayList如何扩容?
public boolean add(E e) {
//第一次添加size是0,+1后是1
//第11次添加size是10,+1后是11
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//第一次添加时minCapacity返回的是10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//第一次添加10-0=10满足。进入grow方法后elementData被换成了[10]的数组。
//第二次添加size是1,+1后变成了2,2-10不满足。。。。。
//第9次添加size是8,+1后变成了9,9-10不满足
//第10次添加size是9,+1后变了10,10-10还是不满足。但是此时elementData这个[10]的数组已经满了。
//第11此添加size是10,+1后变了11,11-10终于满足了。
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//第11次elementData的长度是10
int oldCapacity = elementData.length;
//newCapacity=10+(10>>1)=10+(10/1/2)=15
int newCapacity = oldCapacity + (oldCapacity >> 1);
//条件不满足
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//条件不满足
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//这个方法结束后elementData成了[15],但是此时已经有11个元素存在了。
elementData = Arrays.copyOf(elementData, newCapacity);
}
到这里结果就出来了,当前数组已经满了,再次添加会触发扩容,而扩容的值是当前elementData.length+(elementData.length>>1)
当使用无参构造时默认的长度是10,第一次扩容是在第11个元素添加,扩容后的长度是15.
第二次扩容是在添加第16个元素,扩容后的长度是22.
第三次扩容是在添加第23个元素,扩容后的长度是33.
使用有参构造的必要性!
假设我们的ArrayList中已经知道大概要添加100个元素。按照上边的计算规则,我们看看要扩容几次?
扩容的值是当前elementData.length+(elementData.length>>1)=elementData.length+(elementData.length/2)
当使用无参构造时默认的长度是10,第一次扩容是在第11个元素添加,扩容5,扩容后的长度是15.
第二次扩容是在添加第16个元素,扩容7,扩容后的长度是22.
第三次扩容是在添加第23个元素,扩容11,扩容后的长度是33.
第四次扩容是在添加第34个元素,扩容16,扩容后的长度是49.
第五次扩容是在添加第50个元素,扩容24,扩容后的长度是73.
第六次扩容是在添加第74个元素,扩容36,扩容后的长度是109.
总共需要6次扩容!而扩容就需要创建一个新的数组,将老的数组中的元素全部复制到新的数组中,是特别慢的。
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);
}
}
以上代码是有参构造,可以看出只要你这个参数大于0就能够初始化elementData的数组大小。
理想上,你可以初始化任意大小的容量,实际上你还要看你当前JVM的容量是否允许你这么做。
无论如何,如果你的集合确定需要增加100以上的元素,请使用自定义长度吧。
get(int index)可能抛出两个异常!
/*
返回列表中指定位置的元素。
*/
public E get(int index) {
rangeCheck(index);
//如果检验通过
return elementData(index);
}
/*
检查给定索引是否在范围内。如果不是,则抛出适当的运行时异常。
这个方法不检查索引是否为负:它总是在数组访问之前使用,如果索引为负,
则抛出ArrayIndexOutOfBoundsException。
*/
//所以如果是下标越界是IndexOutOfBoundsException
//如果下标是负数则是ArrayIndexOutOfBoundsException
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//返回elementData数组中的元素
E elementData(int index) {
return (E) elementData[index];
}
set(idx,ele)就是替换数组中对应下标的元素!
/*
将列表中指定位置的元素替换为指定元素。
*/
public E set(int index, E element) {
//首先检查传入的下标,和获取时一样可能会抛出两个异常
rangeCheck(index);
E oldValue = elementData(index);
//根据下标替换对应的元素
elementData[index] = element;
return oldValue;
}
remove(int index),巧妙使用arraycopy(.....)一个本地方法
/*
移除此列表中指定位置的元素。将后续的元素向左移动(从其索引中减去1)
*/
public E remove(int index) {
//检验下标,如果下标>=元素个数则抛出下标越界
rangeCheck(index);
//添加时也有这个操作,目前尚未知道它的意义。
modCount++;
//获取对应下标的元素
E oldValue = elementData(index);
//设当前集合内元素个数为5,remove(0),则5-0-1=4
//设当前集合内元素个数为5,remove(1),则5-1-1=3
//设当前集合内元素个数为5,remove(2),则5-2-1=2
//设当前集合内元素个数为5,remove(3),则5-3-1=1
//设当前集合内元素个数为5,remove(4),则5-4-1=0
int numMoved = size - index - 1;
//由此可见,这个判断肯定是成立的,下标越界已经排除在外,负值是错误的。
if (numMoved > 0)
//关键就是这个方法,扩容复制是也是使用的它。
System.arraycopy(elementData, index+1, elementData, index,numMoved);
elementData[--size] = null;
//返回被删除的元素
return oldValue;
}
//这是一个本地方法,看不到代码。只能根据注释去分析,它干了啥。
//src:源数组,srcPos:从源数组的这个位置开始,dest:挪到目标数组,destPos:目标数组起始位置 length:挪length个元素
//这个方法还是有意思的,咱们来模拟下参数,咱们选择删除的下标是1
//{1,2,3,4,5,null,null,null,null,null},1+1,{1,2,3,4,5,null,null,null,null,null},1,size-index-1=3
//1 从src的第二个位置开始复制,复制3个。{3,4,5}
//2 放到dest数组,从第一个下标开始。{1,3,4,5,5,null,null,null,null,null}
public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
//最后执行size-1是4,将下标是4这个元素值为null。{1,3,4,5,null,null,null,null,null,null}
elementData[--size] = null;
remove(Object o),删除第一个匹配的元素。
public boolean remove(Object o) {
//如果要删除的元素是null,则从头遍历找到第一个null的下标
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
//这个方和remove(int index)类似
fastRemove(index);
return true;
}
} else {
//如果不是null,则调用元素的equals()找到相同元素的下标
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//这个方法主要还是arraycopy(),结束后将最后一个元素置为null
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;
}
三种迭代删除的方式。
1 使用for循环时注意每次删除一个元素需要将自变量-1,否则会跳过元素。
public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); arrayList.add("a"); arrayList.add("b"); arrayList.add("c"); arrayList.add("c"); arrayList.add("d"); arrayList.add("d"); for (int i = 0; i < arrayList.size(); i++) { String s = arrayList.get(i); if (s.equals("c")) { arrayList.remove(i); i--; } } for (int i = 0; i < arrayList.size(); i++) { System.out.println(arrayList.get(i)); } }
2 倒着删除,第一个被删除的是下标为3的,删除后只会影响数组后的,不影响下次删除下标为2的。
public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); arrayList.add("a"); arrayList.add("b"); arrayList.add("c"); arrayList.add("c"); arrayList.add("d"); arrayList.add("d"); for (int i = arrayList.size() - 1; i >= 0; i--) { String s = arrayList.get(i); if (s.equals("c")) { arrayList.remove(i); } } for (int i = 0; i < arrayList.size(); i++) { System.out.println(arrayList.get(i)); } }
3 迭代器删除
public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); arrayList.add("a"); arrayList.add("b"); arrayList.add("c"); arrayList.add("c"); arrayList.add("d"); arrayList.add("d"); Iterator<String> iterator = arrayList.iterator(); while (iterator.hasNext()) { String ele = iterator.next(); if (ele.equals("c")) { iterator.remove(); } } for (int i = 0; i < arrayList.size(); i++) { System.out.println(arrayList.get(i)); } }
//下一个next的角标
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
//cursor默认是0,若集合内有元素,则第一次调用该方法肯定是true
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
//第一次是0
int i = cursor;
//不成立
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
//不成立
if (i >= elementData.length)
throw new ConcurrentModificationException();
//这意味着调用一次next(),指针就会增加1。
cursor = i + 1;
//lastRet表示上一次指针的位置,所以默认是-1,这次结束后是1.
//它返回的就是当前下标位置的元素。
return (E) elementData[lastRet = i];
}
//迭代器的删除
public void remove() {
//小于0则代表没有经过一次next,lastRet还是-1,如果next()一次则lastRet是0
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//这里还是调用ArrayList自己的remove(int index);这里实参是0.
ArrayList.this.remove(lastRet);
//删除后将指针赋值给当前下标,cursor在下次next()用到。
//例如当前调用next()后cursor是1,lastRet是0.则删除0位置的元素后,将cursor指向0
//这么做就类似于我们用for循环把自变量-1的操作。
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
clear()清空元素,让GC释放元素内存
/*从列表中删除所有元素。这个调用返回后,列表将为空。*/
public void clear() {
//其实这个计数器肯定是有用的,往后在看看吧
modCount++;
// clear to let GC do its work
//循环将数组内的元素全部置为null,它的作用其实是端口元素和集合的联系
//让元素可以被回收。
for (int i = 0; i < size; i++)
elementData[i] = null;
//设置size=0,表示集合内没有元素
size = 0;
}