ArrayList源码分析(笔记)

参考资料:B站

1. ArrayList继承体系

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • Serializable:类的序列化由实现java.io.Serializable接口的类启用。不实现此接口的类将不会使任何状态序列化或反序列化。可序列化类的所有子类型都是可以序列化的。序列化接口没有方法和字段,仅用于标识可串行化的语义,也就是标记接口,告诉jvm说我可以序列化,类似注解。该接口如下:

    public interface Serializable {
    }
    

    序列化:是将对象的状态信息转换为可以存储或传输的形式的过程。比如将对象序列化到硬盘,方便网络传输。
    反序列化:将硬盘中某个文件中的数据读取出来,反序列化为一个对象。
    注意:如果该类实现了序列化接口,那么该类里的所有属性都会被序列化和反序列化,不想序列化的字段可以使用transient修饰。

  • Cloneable:它也是标记接口,一个类实现Cloneable接口来指示Object.clone()方法。在不实现Cloneable接口的实例上调用对象的克隆方法会导致异常CloneNotSupportedException被抛出。所谓的克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝。
    克隆的前提条件:1. 被克隆对象所在的类必须实现Cloneable接口。
            2. 必须重写clone()方法。注意clone()方法是Object的。

    基本使用

    //普通类实现了Cloneable接口
    public class Student implements Cloneable{
        private String name;
        private String sex;
        
        @Override
        protected Object clone() throws CloneNotSupportedException {
            //调用Object的clone方法,注意它是native方法,是调用底层C语言的
            return super.clone();
        }
        
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getSex() {
            return sex;
        }
        public void setSex(String sex) {
            this.sex = sex;
        }
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", sex='" + sex + '\'' +
                    '}';
        }
    }
    
    public static void main(String[] args) throws CloneNotSupportedException {
      Student stu = new Student();
      stu.setName("小霆");
      stu.setSex("男");
      //调用克隆方法,复制出一个一模一样的自己
      Object stu1 = stu.clone();
      System.out.println(stu == stu1);//false 两个是不同的对象,是相互独立的
      //对比复制前的内容跟复制后的内容是否一样
      System.out.println(stu);  //Student{name='小霆', sex='男'}
      System.out.println(stu1); //Student{name='小霆', sex='男'}
      //答案是一样的
    }
    

     如果说我Student类不去实现Cloneable接口的话,如下:

    Exception in thread "main" java.lang.CloneNotSupportedException: thread.Student
       at java.lang.Object.clone(Native Method)
    

    分类:clone可以分为浅拷贝和深拷贝。先看浅拷贝,有如下代码:

    //技能类
    public class Skill {
       private String skillName;
       public Skill(){
       }
       public Skill(String skillName){
           this.skillName = skillName;
       }
       public String getSkillName() {
           return skillName;
       }
       public void setSkillName(String skillName) {
           this.skillName = skillName;
       }
       @Override
       public String toString() {
           return "Skill{" +
                   "skillName='" + skillName + '\'' +
                   '}';
       }
    }
    
    public class Student implements Cloneable{
        private String name;
        private String sex;
        private Skill skill;
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getSex() {
            return sex;
        }
        public void setSex(String sex) {
            this.sex = sex;
        }
        public Skill getSkill() {
            return skill;
        }
    
        public void setSkill(Skill skill) {
            this.skill = skill;
        }
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", sex='" + sex + '\'' +
                    ", skill=" + skill +
                    '}';
        }
    }
    

     注意Student类做了改动,里面包含了Skill对象。
     有如下测试:

    在这里插入图片描述 也就是说,当skill的值发生了改变,被克隆的对象stu1的skillName属性值也发生改变。这说明了该拷贝拷的是skill对象的引用。按道理说,我们拷贝一个东西,如果原东西发生改动,是不是不能影响拷贝过来的东西。像这种情况就叫浅拷贝。那么深拷贝能解决这个问题吗?如下,先让Skill类也实现Cloneable接口,并重写clone()方法,这个就不贴代码出来了,然后对Student类的clone()方法做个修改:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //return super.clone(); 深拷贝,不能简单的调用父类的方法
        Student stu = (Student) super.clone();
        Skill skill = (Skill) this.skill.clone();
        stu.setSkill(skill);
        return stu;
    }
    

     再做测试,如下:
    在这里插入图片描述
     所以,我们可以做个总结,浅拷贝是不会拷贝里面的子对象的,而深拷贝会把里面的子对象也拷贝一份,这样,两者所引用的技能对象的地址就不同了。

  • RandomAccess(随机访问)标记接口:表示了实现该接口的类支持快速随机访问。此接口的主要目的是允许通过算法更改其行为,以便在应用于随机访问列表顺序访问列表时提供良好的性能。那什么是随机访问呢?说的简单点,就是可以通过索引去定位一个元素,比如如下的for循环:

    ArrayList arrayList = new ArrayList();
    arrayList.add("a");
    arrayList.add("b");
    for (int i=0;i<arrayList.size();i++){
        System.out.println(arrayList.get(i));
    }
    

     那么顺序访问就是从头到尾依次访问,比如链表的访问方式就是顺序访问。如下的iterator就是顺序访问(当然也包括增强for循环,毕竟增强for循环底层也是iterator):

    ArrayList arrayList = new ArrayList();
    arrayList.add("a");
    arrayList.add("b");
    Iterator iterator = arrayList.iterator();
    while (iterator.hasNext()){
       System.out.println(iterator.next());
    }
    

    然后看一下ArrayList的随机访问和顺序访问哪个的效率高,如下:

    public static void main(String[] args) throws CloneNotSupportedException {
           ArrayList arrayList = new ArrayList();
           for (int i=0;i<100000;i++){
              arrayList.add(i);
           }
           long start = System.currentTimeMillis();
           for (int i=0;i<arrayList.size();i++){
              arrayList.get(i);
           }
           long end = System.currentTimeMillis();
           System.out.println("随机访问所用时间"+(end-start));
           System.out.println("======================");
           long start2 = System.currentTimeMillis();
           Iterator iterator = arrayList.iterator();
           while (iterator.hasNext()){
              iterator.next();
           }
           long end2 = System.currentTimeMillis();
           System.out.println("顺序访问所用时间"+(end2-start2));
    }
    

     经过多次测试,发现用for循环所用的耗时要么比iterator所用的耗时少,要么相等,反正就是不会大于iterator所用的时间,所以说用for循环的这种随机访问的效率更高。
     然后我们再看看链表的吧,代表是linkedList(没有实现RandomAccess),看看它是随机访问快,还是顺序访问快,如下:

    public static void main(String[] args) throws CloneNotSupportedException {
           List linkedList = new LinkedList();
           for (int i=0;i<100000;i++){
              linkedList.add(i);
           }
           long start = System.currentTimeMillis();
           for (int i=0;i<linkedList.size();i++){
              linkedList.get(i);
           }
           long end = System.currentTimeMillis();
           System.out.println("随机访问所用时间"+(end-start));
           System.out.println("======================");
           long start2 = System.currentTimeMillis();
           Iterator iterator = linkedList.iterator();
           while (iterator.hasNext()){
              iterator.next();
           }
           long end2 = System.currentTimeMillis();
           System.out.println("顺序访问所用时间"+(end2-start2));
    }
    

     这次换成了LinkedList之后,随机访问所耗的时间就比顺序访问所耗的时间多了多了。
     所以,以后我们再进行List遍历的时候,就要小心了,是采用随机遍历还是顺序遍历呢?我们不妨在遍历之前先查询返回的结果是否实现了RandomAccess接口,如果实现,就推荐使用随机遍历的方式,否则,就推荐使用顺序遍历的方式。如下:

    if(arrayList instanceof RandomAccess){
       //随机访问
    }else{
       //顺序访问
    }
    
  • AbstractList抽象类:该抽象类实现了List接口,并对一些通用的方法做了重写,所以我们可以采用它的默认实现,直接用,如果想自己重写那就自己重写。

2. ArrayList的构造函数

Constructor描述
ArrayList()构造一个初始容量为10的空列表
ArrayList(int initialCapacity)构造具有指定初始容量的空列表
ArrayList(Collection<? extends E> c)构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序

2.1 无参构造

如下代码:

public static void main(String[] args){
	  //它真的构造了一个初始容量为10的空列表吗?
	   ArrayList arrayList = new ArrayList();
}

 让我们按住Ctrl键,然后鼠标移到ArrayList上,点进去,进入如下代码:

/**
 * Constructs an empty list with an initial capacity of ten.
  */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 我们一眼看上去,是不是一个赋值操作呀。让我们找到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 = {};

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
transient Object[] elementData; // non-private to simplify nested class access

 我们发现DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个常量,类型是Object[],值是{},也就是空数组,我们对其进行直译,就是默认的空容量数组,它的值是不能变的。
 elementData呢类型也是Object[],说明它也是数组,注意这个数组才是集合真正存储数据的容器,也就是说集合的容量(容量是容量,size是size,不是一回事)就是该数组的长度,并且我们都知道ArrayList的底层是数组,那么这个数组就是elementData。我们发现没有,它前面有transient修饰,什么意思,我前面说了,这里不再重复说了。那么此时elementData就是空数组。嗯,怎么没初始化容量为10呢,上面不是说无参构造的初始容量为10吗?其实初始容量为10是当我们第一次add的时候才初始化的。

2.2 有参构造

如下代码:

public static void main(String[] args){
      ArrayList arrayList = new ArrayList(10);
}

 进入源代码,如下:

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

 这个很容易,我们是不是传入了一个10,那么10的传入,就要进行if判断,首先10是不是大于0,那么第一个成立,其它的像else就不走了,那么第一个判断的方法体是不是也是赋值操作,elementData我们已经知道是什么了,而等号右边是不是也非常简单呀,等价替换为:Object[] elementData=new Object[10],所以,是不是初始化了一个容量为10的空数组呀,如果我们传入的是20,是不是就初始化容量为20的数组呀。
 如果我们传入的是0,那么就会进入第二个判断的方法体里,让我们点进EMPTY_ELEMENTDATA,如下:

/**
 * Shared empty array instance used for empty instances.
  */
private static final Object[] EMPTY_ELEMENTDATA = {};

 是不是一个空数组呀,也是常量,跟上面的那个DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一样的,只不过EMPTY_ELEMENTDATA这个是当我们指定为0才给我们返回的,而前面带有DEFAULTCAPACITY_这个单词是不是翻译为默认容量呀,默认默认,就是默认给我们的,就是当我们不指定为0的时候,它就拿DEFAULTCAPACITY_EMPTY_ELEMENTDATA给我们。好,最后不管怎么样,elementData是不是为空。

2.3 有参构造之单列集合

public static void main(String[] args){
   ArrayList arrayList = new ArrayList();
   arrayList.add("aaa");
   arrayList.add("bbb");
   arrayList.add("ccc");
   ArrayList arrayList1 = new ArrayList(arrayList);
   for(Object s:arrayList1){
      System.out.println(s);
   }
}

定位到第6行,进去:

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

 首先,集合调用toArray方法,就是把集合转换为数组,注意,它的底层是Arrays.copyOf,所以它其实是创建了一个新数组,该新数组的长度跟集合的长度是一样的,并且值也是一样的,就是复制了一份嘛。注意哈,我并没有说容量一样。如下:

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}
public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

 首先,定位到上述源代码的第3,4,5行上,认真观察就会发现等号右边的是三目运算,并且看结果可以知道,不管结果如何,它都会创建一个新的数组,这个数组跟我们的elementData可是不一样的,也就是说是两个对象。好,然后通过参数可知,它们都有newLength的参数,这个newLength就是集合的长度size,所以为3,也就是说,它创建了一个Object类型的数组,容量为3。也就是这样的,如下:

Object[] copy = new Object[3];

 注意变量我用copy接收。
 再来看看System.arraycopy是什么东西,如下案例:

public static void main(String[] args) {
  //arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
   /*
       Object src : 原数组
       int srcPos : 从元数据的起始索引开始
      Object dest : 目标数组
      int destPos : 目标数组的开始起始索引
      int length  : 要copy的数组的长度
   */
   int[] array = new int[10];
   for(int i=1;i<=5;i++){
       array[i-1] = i;
   }
   for(int i=0;i<array.length;i++){
       System.out.print(array[i]+",");
   }
   System.out.println();
   System.arraycopy(array,1,array,2,4);
   for(int i=0;i<array.length;i++){
       System.out.print(array[i]+",");
   }
}

 运行结果如下:

1,2,3,4,5,0,0,0,0,0,
1,2,2,3,4,5,0,0,0,0,

 我们通过画图来分析上述结果是怎么出来的,如下:
在这里插入图片描述 然后我们把源代码的等价替换过来,如下:

System.arraycopy(elementData, 0, copy, 0,
                     Math.min(elementData.length, 3));

 Math.min说一下,它是对比两个参数,看两个参数哪个小,就返回谁。
 一样画图分析,如下:
在这里插入图片描述
 总结:1. 先把新数组创建出来。2,再用System.arraycopy进行赋值。
 最终是不是一直把copy数组返回出去呀,返回到哪?就是如下这句:

elementData = c.toArray();

 那么此时的elementData就是新数组,也就是那个copy。然后往下,就是判断了,它会取新数组的长度,然后赋值给size,再判断它是否并不等于0,如果不等于0,那么进去,然后下面这个判断是为了防止c.toArray()返回的不是Object[].class,也就是说,我们传进来的arrayList很有可能重写了toArray()方法,并且toArray()返回的不是Object[]类型的,而是String[]类型之类的,这样的话,如果遇到向下转型的话就会报错。到此为止,arrayList1的elementData就有值了,所以arrayList1这个集合就复制出来了。

3. add方法

3.1 add(E e)

如下代码:

public static void main(String[] args) {
   ArrayList<String> list = new ArrayList<String>();
   list.add("aaa");
}

定位到第3行,进入源码,如下:

public boolean add(E e) {
   ensureCapacityInternal(size + 1);  // Increments modCount!!
   elementData[size++] = e;
   return true;
}

 size为0,注意这个size表示集合的长度,此时集合里还没有元素,字符串aaa还没添加进去呢,它执行到第二行才会添加进去,很明显是不是size++呀,这个很简单,重点放在第一行上,让我们进去ensureCapacityInternal方法,如下:

private void ensureCapacityInternal(int minCapacity) {
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

DEFAULTCAPACITY_EMPTY_ELEMENTDATA上面已经见到过了,就是来判断elementData是否为空数组,或者说你这个elementData数组的容量是否为空,这样说更准确,因为上面我们是不是学过ArrayList的无参和有参?,而且在无参那节我是不是说过它会初始化一个容量为10的数组,但是是在add的时候才初始化的,还记不记得?那就没错了,看我的案例,是不是new了一个无参的ArrayList,所以容量就为空,我再把无参的源代码贴出来,免得你又翻回去:

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 看到没有,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是不是赋值给elementData,所以我上上面源代码里的判断是不是成立的,是不是为true,为true的话,我是不是可以进入判断体里,所以,明白了吧,这个判断就是判断你的容量是否为空,如果为空,我就把10传递给你,而这个10就是我下面要说的DEFAULT_CAPACITY常量。
 那好,进入,首先,你要明白Math.max()表示什么意思,它跟上面说到的Math.min()是相反的,这个是返回最大的那个,所以,不做过多解释,可以点进去看它的源码,很简单,我就不点了。
 好,然后我们看看DEFAULT_CAPACITY,直译为默认容量,它是一个常量,值为10。而minCapacity是不是我们传进来的参数呀,size是不是为0,那么0+1就是1,很明显,1比10小,所以,最终返回的是10,重新赋值给minCapacity。最后带着minCapacity又传入了一个方法,注意这个方法将真正决定ArrayList是否真的要扩容,如下:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

 modCount就是记录你实际修改集合的次数。
 好,进行判断(注意,minCapacity是size+1,为什么size要加1,因为它要判断你这个数组如果添加进来一个元素,会不会导致数组容量不够,比如数组容量是10,而你集合的长度假设也是10,那么根据判断10-10等于0,0不大于0,所以为false,就不会去执行里面的grow方法,也就不会扩容,那么我加1测试一下,11-10不就大于0了吗,说明容量不够了,我要扩容,所以,明白了吗?),当前elementData.length为0,也就是容量为0,所以10-0就是10,10比0大,所以成立,进去,执行grow方法,注意这个grow方法就是真正来扩容的,也就是说,如果该判断成立,我们就扩容,如果不成立,我们就不扩容。而当前这个elementData是不是必须扩容呀,不然怎么添加元素进去(否则会报ArrayIndexOutOfBoundsException异常),所以让我们进入grow方法吧,如下:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

 这个grow就是来真正的扩容了,我们平时不是会说ArrayList是一个动态数组吗,动态在哪?就在这个grow方法里,知道存不下了,我就自动扩容,这就是动态。
 好,看上述源代码的第3行,很好说,oldCapacity就为0,好,执行第4行,这个第4行要注意了,等号右边的运算决定了你的数组要扩大到多少。然后看看oldCapacity >> 1这个东西,这个我们就暂时理解成oldCapacity除以2,那么0除以2是不是还是0,0+0也为0,所以newCapacity就为0。但是我强调一遍,等号右边的运算其实等价于oldCapacity*1.5,也就是说,它扩容,其实是扩容原数组的1.5倍,这个很重要,那么0*1.5是不是也一样是0呀,所以newCapacity为0是跑不掉的。继续向下执行,到第5行,0-10是不是一个负数,负数是不是小于0,成立,进去,那么newCapacity的值就不再是0了,而是10。然后再执行第7行判断,这个其实表示如果你当前的这个容量比数组的最大值还要大,那么它就会启动一个hugeCapacity来处理,这个不管。那么继续向下,是不是就到了第10行呀,Arrays.copyOf()这个函数上面说过,它是不是传了两个参数进去,等价替换为:elementData = Arrays.copyOf(elementData, 10);,也就是说,它会把elementData当做原数组,然后把10传进去是因为它底层要创建一个容量为10的数组,接下来是不是要System.arraycopy进行复制呀,但是我原数组里一个元素也没有,所以赋值也没用,所以它就把一个容量为10的数组进行返回,重新赋值给elementData。
 也就是说,当我们需要扩容的时候,就会创建一个新数组,新数组的容量是我们在grow()方法里计算好的,然后就会把原数组的所有数据一一复制进新数组,这就是扩容的流程
 所以,扩完之后,elementData的长度就为10了。也就是此时该数组索引为0到9,每个元素都为null。到elementData[size++] = e;这一行,到这为止,因为elementData的容量不再为0了,既然不为0了,那么我们就可以往里添加元素了,注意在添加之前,size还为0,同时要注意它是后加加,这个很容易看懂,没啥好说的,然后就return true。

3.2 add(int index,E element)

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<String>();
    list.add(1,"aaa");
}

 点击进入,如下:

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

 然后进入rangeCheckForAdd方法,如下:

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

 也就是说,它会判断我们传进来的索引有没有大于集合的长度,或者有没有小于0。很明显,index是1,size是0,所有成立,要抛异常,如下:

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
	at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:661)
	at java.util.ArrayList.add(ArrayList.java:473)
	at com.cht.ioc.TestIOC.main(TestIOC.java:9)

 也就是说我们用这种方法在添加元素的时候要按索引顺序添加,不能跨索引添加,比如如下:

 ArrayList<String> list = new ArrayList<String>();
 list.add(0,"aaa");
 list.add(1,"aaa");
 list.add(3,"aaa");

 1跟3之间是不是少了个2呀。不是连续的,要报错。也不能这么说,应该是你在添加的时候要顺序添加,除非你是插队的,比如如下:

 ArrayList<String> list = new ArrayList<String>();
 list.add(0,"aaa");
 list.add(1,"bbb");
 list.add(2,"ccc");
 list.add(1,"ddd");
 System.out.println(list);

 这个就不会报错,因为你最后添加的虽然是索引1,但是它并不大于集合的长度呀,所以就不会报错,你把1换成4就不行啦,注意这个是插队,为什么是插队,因为你元素ddd要插入到bbb的前面。结果如下:

[aaa, ddd, bbb, ccc]

 好,回归正题,那么我们把index换成0就ok了,如下:

ArrayList<String> list = new ArrayList<String>();
list.add(0,"aaa");

 好,进入源代码,执行ensureCapacityInternal方法,这个方法其实跟上面讲的add(E e)是一样的,也是看看是否要扩容,要,就执行grow方法,不用,那就不用执行grow方法。很显然,是要扩容的,那么就会扩容一个容量为10的数组。然后下一个是System.arraycopy,说白了,你不是要在索引为0的位置添加一个元素吗,那么我就在原数组索引为0的位置开始往后后退1位,为你的新元素添加进去,说白了就是我前面说的插队。那么下一句elementData[index] = element;就可以开始真正的赋值了。然后size++,没什么好说的。

3.3 addAll(Collection<? extends E> c)

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("a");
    list.add("b");
    list.add("c");
    ArrayList<String> list1 = new ArrayList<>();
    list1.addAll(list);
    System.out.println(list);
    System.out.println(list1);
}

 我们直接定位到第7行上,然后进去,如下:

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

 我们遇到了c.toArray(),一样,c就是list,此时这个list的size为3,是有元素的,然后我们调用toArray()方法,这个方法我们说过了,就是复制一份数组,跟list是一模一样的,只是容量不一样。再赋值给Object[] a。然后下一步,就是把a的长度赋值给numNew,也就是numNew为3。下一句,注意这里的size是新集合的长度,也就是0,0+3就是3,传入一个函数里,而该函数上面也见到过了,也是判断是否要扩容的。好,再下一句,System.arraycopy,原数组是不是a,是有值的啊,也就是list,目标数组是elementData,这个是空数组的,也就是list1,我们可以近似的把它看做这样的:System.arraycopy(list, 0, list1, 0, 3);,从原数组的第0个索引开始复制3个到list1数组的索引为0的位置。那么此时此刻elementData就有值了。后面size+=numNew比较简单,就不说了。
 总之一句话,就是先把你原数组的长度和目标数组的长度加起来,然后把你加起来的长度看做是一个数组的长度,检验一下是否需要扩容,如果需要,就扩,不需要,就不扩,这样可以确保elementData的容量,为下一句System.arraycopy做准备,否则就会报数组越界异常,也就是ArrayIndexOutOfBoundsException,这样你在拷贝数据从a到elementData的时候就不用担心数组越界了,这点要注意。

3.4 addAll(int index,Collection<? extends E> c)

public static void main(String[] args) {
     ArrayList<String> list = new ArrayList<String>();
     list.add("a");
     list.add("b");
     list.add("c");
     ArrayList<String> list1 = new ArrayList<String>();
     list1.add("HAHA");
     list1.add("haihai");
     list1.addAll(1,list);
     System.out.println(list);
     System.out.println(list1);
}

让我们定位到第9行,然后进去,如下:

public boolean addAll(int index, Collection<? extends E> c) {
   rangeCheckForAdd(index);

   Object[] a = c.toArray();
   int numNew = a.length;
   ensureCapacityInternal(size + numNew);  // Increments modCount

   int numMoved = size - index;
   if (numMoved > 0)
       System.arraycopy(elementData, index, elementData, index + numNew,
                        numMoved);

   System.arraycopy(a, 0, elementData, index, numNew);
   size += numNew;
   return numNew != 0;
}

 第2行,不用说,是在校验索引index,上面说过了。看第4行,一样,将集合(list)转数组,然后赋值给a,那么a的长度就是3,也就是numNew为3,好,继续,下一行,一样,看看是否需要扩容,为System.arraycopy做好准备,不说了,跟上面一样。看第8行,size是list1的size,为2,index呢就是你传入的第一个参数的值,为1,那么2-1就是1,也就是numMoved为1,这个numMoved呢它表示要移动的元素的个数,说白了,你看下面System.arraycopy的最后一个参数就明白了,最后一个参数表示什么意思,它就是什么意思,没什么,可以自己私底下想一下,比如有10个元素,我要在索引为3的位置插入一个元素,那么原数组以索引为3的元素是不是要整体往后挪呀,那么总共有多少个元素在往后挪,根据它的算法size-index,也就是10-3,等于7,好,举起你的双手,数一下,是不是有7个元素在往后挪。
 看第13行,注意上面的那个System.arraycopy是你list1上的自身移动,你list上的数据还没添加进来呢,所以是不是要把list上的数据添进来呀。System.arraycopy这个方法大家自己再好好感受下,可以画下图,可能比较难懂点,但想多了就不觉得难,其实很简单。

4. set方法

4.1 set(int index,E element)

public static void main(String[] args) {
  ArrayList<String> list = new ArrayList<>();
   list.add("a");
   list.add("b");
   list.add("c");
   list.add("d");
   String value = list.set(3,"dddd");
   System.out.println("set方法返回值:"+value);
   System.out.println("集合的元素:"+list);
}

 让我们定位到第7行,然后进去,如下:

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

 rangeCheck的话不妨进去看一下,如下:

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

 也就是说,你传进来的的索引不能大于我的数组容量,否则抛数组越界异常,很简单,退出。看下一句,也就是第4行,进去,如下:

E elementData(int index) {
   return (E) elementData[index];
}

 这个也简单,就是把你原数组的那个值取出来。你看第6行,是不是reture出去了,这不用说吧,set()的返回值就是旧数据,这是基础知识。然后第5行是不是就是真正的更改值了呀,也非常简单。

5. get方法

5.1 get(int index)

public static void main(String[] args) {
   ArrayList<String> list = new ArrayList<>();
   list.add("a");
   list.add("b");
   list.add("c");
   list.add("d");
   String value = list.get(1);
   System.out.println(value);
}

 定位到第7行,进入源代码,如下:

public E get(int index) {
   rangeCheck(index);

   return elementData(index);
}

 好了,没啥。

6. toString方法

ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
String value = list.toString();
System.out.println(value);

 定位到6行,点进去,如下:

public String toString() {
    Iterator<E> it = iterator();
    if (! it.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = it.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! it.hasNext())
            return sb.append(']').toString();
        sb.append(',').append(' ');
    }
}

 注意注意,这个方法是在AbstractCollection这个抽象类中的,ArrayList是没有toString方法的,也就是说ArrayList是AbstractCollection的一个子类。第2行先不说,先看第3行,这个判断就是判断list集合是否有元素,有元素,那我们就不进入,好,跳到StringBuilder sb = new StringBuilder();这里,不用说了,是用来拼接字符串的,好,继续向下,首先,先追加[,这个不用说,然后进入for(;;)循环,其实是while循环,这应该都懂,进入,先把第一个数据取出来,赋值给e,再向下,看e是否恒等于this,this就是当前对象啦,比如如下:
在这里插入图片描述 后面的判断就不用说了,简单。

7. Iterator迭代器

public static void main(String[] args) {
   ArrayList list = new ArrayList();
   list.add("java");
   list.add("php");
   Iterator iterator = list.iterator();
   while(iterator.hasNext()){
       System.out.println(iterator.next());
   }
}

 定位到第5行,然后进去,如下:

public Iterator<E> iterator() {
    return new Itr();
}

 进入Itr类(ArrayList的内部类),如下:

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;

     Itr() {}

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

     public void remove() {
         if (lastRet < 0)
             throw new IllegalStateException();
         checkForComodification();

         try {
             ArrayList.this.remove(lastRet);
             cursor = lastRet;
             lastRet = -1;
             expectedModCount = modCount;
         } catch (IndexOutOfBoundsException ex) {
             throw new ConcurrentModificationException();
         }
     }

     @Override
     @SuppressWarnings("unchecked")
     public void forEachRemaining(Consumer<? super E> consumer) {
         Objects.requireNonNull(consumer);
         final int size = ArrayList.this.size;
         int i = cursor;
         if (i >= size) {
             return;
         }
         final Object[] elementData = ArrayList.this.elementData;
         if (i >= elementData.length) {
             throw new ConcurrentModificationException();
         }
         while (i != size && modCount == expectedModCount) {
             consumer.accept((E) elementData[i++]);
         }
         // update once at end of iteration to reduce heap write traffic
         cursor = i;
         lastRet = i - 1;
         checkForComodification();
     }

     final void checkForComodification() {
         if (modCount != expectedModCount)
             throw new ConcurrentModificationException();
     }
}

 当我们去new上面这个类的时候,它会去初始化上面的三个成员变量,要注意。
 cursor代表光标,或者叫指针,默认值为0,通过注释翻译为"要返回的下一个元素的索引"。lastRet 这个东西通过它的注释可以知道,它是返回最后一个元素的索引的,如果没有,则返回-1。expectedModCount = modCount;是不是将集合实际修改次数赋值给预期修改次数。

 好,让我们走到while(iterator.hasNext()){这里吧,进去,如下:

public boolean hasNext() {
    return cursor != size;
}

 它呢是判断光标是否不等于集合的size,cursor是0,size是2,0!=2,成立,说明集合有元素,返回true。开始执行iterator.next()代码,进入next,如下:

@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];
}

 看checkForComodification方法里的判断,它是判断预期修改集合次数是否和实际修改集合次数一样,进去看一下:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

 答案是一样的,刚刚才赋好的值嘛。那么为什么要判断一下,你看它里面所抛的异常,翻译为并发修改异常,也就是说在多线程环境下可能出现的异常,什么意思?也就是说,当我们在遍历集合的时候,突然有另一个线程调用了remove()方法删除了集合中的某一个元素,那么就可能会造成modCount和expectedModCount不相等,注意是可能,后面会分析。
 继续往下看,这时i就等于0,继续往下,看i是否大于等于集合的size,如果大于或等于说明没有元素了,很明显不大,继续往下,走到Object[] elementData这一行,ArrayList.this就相当于list这个集合,因为是要调用外部类中的elementData,所以不能直接this。然后把这个数组的地址赋值给elementData这个变量,再进入下一个判断if (i >= elementData.length),这个也是针对多线程环境下并发修改的。然后光标,就要向下移动了,看,是不是加1了,然后从数组中取出元素返回出去。

7.1 并发修改异常产生的原因分析

public static void main(String[] args){
    ArrayList list = new ArrayList();
    list.add("java");
    list.add("php");
    Iterator iterator = list.iterator();
    while(iterator.hasNext()){
        Object o = iterator.next();
        if(o.equals("php")){
            list.remove("php");
        }
    }
    System.out.println(list);
}

 结果如下:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at thread.Test.main(Test.java:12)

 其实根据它的错误提示,我们可以定位到某一行,比如第2行,在ArrayList类的第909行,也就是checkForComodification方法。我们先一步步分析,首先,我们先点位到第6行,进去,也就是如下:

public boolean hasNext() {
   return cursor != size;
}

 size代表集合的长度,为2,好说。看cursor,因为是刚刚初始化,所以为0,那么0不等于2,为true,那么就把true返回,成功进入while方法体,开始执行next()方法,如下:

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();
}

 先判断modCount跟expectedModCount是否相等,因为数组并没有添加元素啥的,还是原来的2(add的时候modCount会加加),所以modCount并不会加加,所以是一样的,相等的,所以判断不成立,那么就不会抛出错误啦。cursor为0,前面说过,然后赋值给i,然后判断i是否大于等于数组长度,是为了防止没有该索引,比如,如下:

public static void main(String[] args){
    ArrayList list = new ArrayList();
    Iterator iterator = list.iterator();
    System.out.println(iterator.next());
}

 像上面这段代码,就会抛出NoSuchElementException。
 让我们继续,elementData就不说了,下一句,判断i >= elementData.length是否成立,目前是不成立的,所以没进去,继续往下走,就是cursor加1了,那么就意味着指针也向下走了,所以安全返回。
 安全返回后是不是赋值给o,然后是不是判断o是否等于php,很明显,是不等于的,所以不进去。那么继续while循环,还是先执行hasNext(),1并不等于2,所以成立,进入while方法体,开始执行next()方法,modCount还是等于expectedModCount,还是那句话,集合并没有发生任何改变。然后i为1,cursor变为2,这些我就一笔带过,好,现在重点来了,要来执行list.remove("php");语句了,那么,我们就点进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;
}

 这个就很简单了,直接看else吧,是不是要遍历集合呀,看看你传入的php在我集合里有没有,如果有,我就进去调用fastRemove方法,所以,让我们点进fastRemove看看吧,如下:

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
}

 相信到这,你就明白了,为什么会抛出并发修改异常,因为你modCount加加了,那么,你下次再去next的时候,它发现你modCount和expectedModCount不一致,那么就真正的抛出ConcurrentModificationException异常了,所以,明白了吗?下面的就是把我们要删除的数据置为null。
 好,让我们继续,它又一次执行hasNext,注意cursor为2,那么size呢,因为我们删除了一个,所以为1,2并不等于1,所以成立,到执行next()中的checkForComodification方法就报错了。

7.2 并发修改异常的特殊情况

public static void main(String[] args){
    ArrayList list = new ArrayList();
    list.add("php");
    list.add("java");
    Iterator iterator = list.iterator();
    while(iterator.hasNext()){
        Object o = iterator.next();
        if(o.equals("php")){
            list.remove("php");
        }
    }
    System.out.println(list);
}

 注意,这里我把添加顺序交换了,原本php是在java的后面的,现在它在java的前面,就这点改变,其它不变,然后看看运行结果,如下:
 奇怪,这是为什么呢?那么我们就从hasNext开始点进去看看吧,如下:

public boolean hasNext() {
    return cursor != size;
}

 cursor为0,size为2,所以为true,进去while方法体,开始执行next()方法,这个就不多说了,然后remove,那么modCount就会加加,好,再次hasNext,cursor为多少?因为你刚刚执行了一次next(),所以cursor就为1,size因为你刚删除了1个,所以2-1为1,所以结果为false,为false,就不进去啰,那么就会退出while循环,根本就接触不到next()方法中的checkForComodification方法。

结论

 当要删除的元素在集合的倒数第二个位置的时候,不会产生并发修改异常,原因是因为在调用hasNext方法的时候光标的值和集合的长度一样,那么就会返回false,因此就不会再去调用next方法获取集合的元素,因为没有去调,所以底层就不会产生并发修改异常。

7.3 Iterator中默认的remove()方法

 注意,不是ArrayList中的remove方法,而是Iterator类给我们提供的remove方法,也就是说,用它自身给我们提供的remove方法,就可以避免上述的并发修改异常,是不是这样呢,可以测试一下,如下:
 注意哈,我上述添加的顺序交换过来了,也就是php在后,也就是7.1节的那种情况。好,看结果,是不是并没有报酬,输出的list中php是不是删了,那么它的底层到底是怎么做的呢?如下:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

 lastRet是不小于0的,所以不会抛IllegalStateException异常,checkForComodification就不说了,都知道,反正不会抛异常就是了,继续向下,注意,ArrayList.this.remove调的是ArrayList的方法,不是Iterator里remove的方法。然后注意lastRet的值为1,因为我上面经历了两次next(),是不是每次return,lastRet总会被赋值呀,好,那么它就会把1当成索引传进去,因为它是根据索引删除的,那么既然是根据ArrayList里的remove方法删除的,那么它的modCount就会加加。继续,是不是就把lastRet赋值给cursor,那么cursor原先是2,现在就为1。继续向下,lastRet又变为-1了,为什么变为-1?因为你是不是有可能调了两次Iterator里的remove方法,你看源代码,它一进来是不是叫要判断你的lastRet是否小于0,所以,明白了吗,就是防止你重复删除。好,下一句,就把modCount赋给expectedModCount,这是最最最关键的代码了,因为这样modCount的值跟expectedModCount不就相等了吗?
 一句话概括:其实Iterator里的remove方法底层调的还是ArrayList的remove()方法,只是它把实际修改的次数赋值给了预期修改的次数,所以导致两者相等

8. remove()

 这里我要说的是ArrayList里的remove方法,我们就把对应的源代码拿过来吧,如下:

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
}

 假设size等于6,index等于2,elementData是[1,2,3,4,5,6]。
 OK,根据上面提供的值,我们来仔细分析它是怎么移位的,同时有什么缺点。首先,numMoved为3,没问题,好,进入执行System.arraycopy,等价替换为:System.arraycopy(elementData, 3, elementData, 2, 3);,也就是说,在elementData的索引为3的数组上开始复制,复制3个,到自身索引为2的位置覆盖,说白了,是不是从索引为3的往后开始向前移动1位呀,这样是不是就把索引为2的值给覆盖了,这样就达到删除的效果啦。但是这样删除有什么问题呢?如下:

public static void main(String[] args) {
    ArrayList arrayList=new ArrayList();
    arrayList.add("1");
    arrayList.add("2");
    arrayList.add("3");
    System.out.println(arrayList.toString());
    for (int i=0;i<arrayList.size();i++){
        arrayList.remove(i);
    }
    System.out.println(arrayList.toString());
}

 结果如下:

[1, 2, 3]
[2]

 根本删不干净呀,这是怎么回事呢?我们画图分析一下:
在这里插入图片描述 那么如何解决?应该是从后往前删,如下代码:

public static void main(String[] args) {
   ArrayList arrayList=new ArrayList();
   arrayList.add("1");
   arrayList.add("2");
   arrayList.add("3");
   System.out.println(arrayList.toString());
   for (int i=(arrayList.size()-1);i>=0;i--){
       arrayList.remove(i);
   }
   System.out.println(arrayList.toString());
}

 自己琢磨。。

9. clear()

ArrayList list = new ArrayList();
list.add("java");
list.add("php");
System.out.println(list);
list.clear();
System.out.println(list);

 效果如下:

[java, php]
[]

 进入源码,如下:

public void clear() {
	modCount++;
	
	// clear to let GC do its work
	for (int i = 0; i < size; i++)
	    elementData[i] = null;
	
	size = 0;
}

 没什么好说的,通过for循环把每个元素的值置位null,最后size为0。

10. contains(Object o)

public static void main(String[] args){
    ArrayList list = new ArrayList();
    list.add("java");
    list.add("php");
    if(!list.contains("C#")){
        list.add("C#");
    }
    System.out.println(list);
}

 进入源代码,如下:

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

 再进入indexOf,如下:

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

 只看else,因为我们传进来的不是空,好,我们是不是要进行for循环遍历呀,找到有没有一个东西叫C#的,很遗憾,没有,没有就退出if判断,直接return一个-1到上一级,那么-1大于等于0吗?是不是为false,那好,返回false,有返回上一级,是不是取反,取反就变为真了,为真,就进入执行add("C#");操作,这个就不用说了。

11. isEmpty

public static void main(String[] args){
    ArrayList list = new ArrayList();
    list.add("java");
    list.add("php");
    boolean empty = list.isEmpty();
    System.out.println(empty);
}

 结果为false。好,查看源码,如下:

public boolean isEmpty() {
        return size == 0;
}

 不用说了,贼简单。

12. debug案例演示

 总结:

  • ArrayList中维护了一个Object类型的数组elementData,也就是它把数据放到elementData数组里面,它的底层是数组。
  • 当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
  • 如果使用的是指定大小的构造器(比如new ArrayList(8),表示指定大小为8位的长度),则初始化elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CollectionMethod {
    @SuppressWarnings({"all"})
    public static void main(String[] args) {
        ArrayList list1 = new ArrayList();
        for (int i=1;i<=10;i++)
        {
            list1.add(i);
        }
        for (int i=11;i<=15;i++)
        {
            list1.add(i);
        }
        list1.add(100);
        list1.add(200);
        list1.add(null);
    }
}

开始分析,如下:
然后点击蓝色向下箭头Step Into,进入如下界面:
在这里插入图片描述 先说一下怎么用debug,主要说两个,一个是Step into,一个是Step over。Step into遇到子函数就会进入该函数执行,比如上面我new了一下ArrayList(),是不是就进入ArayList()的这个构造函数呀。Step over呢是不会进入子函数里的,但是子函数是会执行的,只是单纯的没进入而已,就往下走了。说白了,就是一个遇到函数就会进去执行再出来,一个就算遇到也不会进去,而是继续往下,一条条顺序执行
 如果说你Step Into无效,也就是不能进入以上界面,那么就按照如下操作就好了,如下图所示:
在这里插入图片描述 好,继续,点击Step over,如下:
在这里插入图片描述DEFAULTCAPACITY_EMPTY_ELEMENTDATA说过了,但当做复习,进去一下吧,如下:
在这里插入图片描述 结论:当我们用无参构造器去初始化ArrayList的时候,默认的数组长度就为0。注意,不是一上来就初始化10,而是添加。
 然后继续向下(Step into),就会回到原处,再向下就进入for循环,如下:
在这里插入图片描述 现在要来添加元素了,但注意,因为是数值型的,所以它要进行一下包装,包装成Integer类型,我们可以Step into进入,如下:
在这里插入图片描述 一直点Step into,就会进入add方法,如下:
在这里插入图片描述 分析一下ensureCapacityInternal,我们进入,如下:
在这里插入图片描述 不说了,继续:
在这里插入图片描述  10跟1比,谁大,是不是10大呀,那么minCapacity是不是就是10,然后继续向下,又会进入一个方法,然后把10传进去,来确定是不是真的要扩容,我们进入该方法,如下:
在这里插入图片描述  继续:
在这里插入图片描述 扩完之后,elementData的长度就为10了。也就是此时该数组索引为0到9,每个元素都为null。那么elementData既然是10了,下一次再进入该方法的时候是不是就是以1.5倍的方式扩容呢?不妨自己试一试。结果当然是的。
 然后一直按Step over返回,到如下界面:
在这里插入图片描述 现在elementData就有空间了,原先是空的,现在就不是空的了,不是空的就可以添加数据进去了,注意此时的size还是0,那么就把数据真正的加入到elementData里去了。往下执行一步,如下:
在这里插入图片描述  再返回,就到头了。其它以此类推。然后我们进入下一个for循环,如下:
在这里插入图片描述  注意,经过上次for循环,10个位置已经填满了,那么这一次就要扩容了,并且是按1.5倍扩容。后面就自己去调了,我懒,就不把图一个个截下来了。下一句,如下:
在这里插入图片描述  经过上一次的15次循环,又满了,又需要扩容了,扩容多少个,那么就看还没添进去元素之前的数组长度为多少,是不是15,那么下一次的扩容的长度是不是15+(15/2)呀。最后注意,我们在Debugger的时候,为了减少某些不必要的麻烦,我们要做如下操作:

13. 补充

13.1 如何复制某个ArrayList到另一个ArrayList中去?

  • 使用clone()方法。
  • 使用ArrayList构造方法。
  • 使用addAll方法。

13.2 ArrayList和LinkedList的区别

  • 数据结构不一样,ArrayList底层是数组,LinkedList底层是双向链表。
  • ArrayList因为实现了RandomAccess标记接口,所以ArrayList支持随机访问,而LinkedList不支持。
  • 对于频繁读取集合中的元素,推荐使用ArrayList,而对于频繁的插入和删除,推荐使用LinkedList。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值