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));
        }
    }
View Code

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));
        }
    }
View Code 

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));
        }
    }
View Code

//下一个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;
}

 

posted @ 2020-08-23 14:40  顶风少年  阅读(134)  评论(0编辑  收藏  举报
返回顶部