• 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;
    }

  • 相关阅读:
    笔记
    Dwarf Tower
    第k小数
    np
    noi 抓住那头牛
    HDU 1575 Try A
    acm 易错警示
    E
    魔改森林 题解(dp+容斥+组合数学)
    F. Unusual Matrix 题解(思维)
  • 原文地址:https://www.cnblogs.com/zumengjie/p/13538394.html
Copyright © 2020-2023  润新知