一句一句的读ArrayList源码(代码基于JDK11)

在我们的面试中ArrayList也是一个很基础的知识点,本文就带你一起学习ArrayList的部分源码。如果有不正确的地方,欢迎指正。祝你学习愉快。

由于网上大部分的解析都是基于JDK8的,然而我的JDK是11。所以我就来写一篇ArrayList在JDK11中的源码剖析。如果8的和11的变化较大,我还会写关于8和11的区别

源码部分较多,一次读不完,可以收藏了下次再读

对应的知识点总结:
ArrayList原理讲解




1、ArrayList类的继承关系


1.1、ArrayList对应的类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCmAllti-1585816341523)(ArrayList%E6%BA%90%E7%A0%81.assets/image-20200401185749886.png)]

其中ArrayList继承了List接口,AbstractList也继承了List接口,这一点历史,我在HashMap的源码分析中,已经阐述过,这里就不再重复。

我们发现ArrayList继承了AbstractList,并且实现了三个不同的接口RandomAccess、Cloneable、Serializable



1.2、AbstractList类

AbstractList类为ArrayList的直接父类,也是最终要的父类,待会在源码分析中我们会进行重点讲解



1.3、RandomAccess接口类

RandomAccess接口是一个标记接口,用以标记实现的List集合具备快速随机访问的能力。

public interface RandomAccess {
}

在我们的数据访问中有随机访问顺序访问两种:

随机访问(通过索引访问)

for (int i=0, n=list.size(){
    i < n; i++) list.get(i);
}

顺序访问(迭代器访问)

for (Iterator i=list.iterator(); i.hasNext(); ){
    i.next();
} 

在数据访问的集和ArrayList和LinkedList中,他们分别在不同的访问方式上占优。ArrayList随机访问更快,LinkedList顺序访问更快。而RandomAccess接口的作用就是为了区分这两个集和,以此能够让我们在实际业务代码中到底使用那种访问方式。例如:

if (list instanceof RandomAccess) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
} else {
    for (User user : list) {
        System.out.println(user);
    }
}

LinkList与ArrayList的区别:LinkList是一个基于双向链表的数据结构,ArrayList是一个基于动态数组的数据结构

LinkList的随机访问数据源码:

//每次LinkedList对象调用get方法获取元素,都会执行以下代码 
list.get(i);

public E get(int index) {
    //判定输入的index节点是否有效(方法详情,请查看补充方法一、二)
    checkElementIndex(index);
    //执行node()方法,返回对应的数据(方法详情,请查看补充方法三)
    return node(index).item;
}

补充方法一:

//补充方法一
private void checkElementIndex(int index) {
    //如果不是正常范围的索引,就抛出异常
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

补充方法二:

//补充方法二
private boolean isElementIndex(int index) {
    //如果传入的index节点大于等于0并且小于链表长度就返回true
    return index >= 0 && index < size;
}

补充方法三:

//补充方法三
Node<E> node(int index) {
    //将链表的总长度右移以为(长度/2)再和需要查找的索引进行比较
    if (index < (size >> 1)) {
        //如果节点在前半部分,就从链表的头部开始往后遍历查找
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        //如果节点在后半部分,就从链表的尾部开始往后遍历查找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

LinkList的顺序访问数据源码:(顺序有点乱,建议耐心看完)

//使用迭代器遍历该链表,首先需要创建一个迭代器(具体方法详情,查看补充方法四)
Iterator<String> iterator = strings.iterator();
//判断下一个元素是否为空(return nextIndex < size)
while (iterator.hasNext()){
    //依次遍历链表节点(具体方法详情,查看补充方法六)
    s = iterator.next();
    //打印遍历出来的数据即可
    System.out.println(s);
}

补充方法四:

//调用迭代器,返回listIterator方法
public Iterator<E> iterator() {
    return listIterator();
}
//调用listIterator方法,返回listIterator()方法
public ListIterator<E> listIterator() {
    return listIterator(0);
}
public ListIterator<E> listIterator(int index) {
    //用于检测索引是否符合规范(具体方法详情,查看补充方法五)
    checkPositionIndex(index);
    //实例化一个带参的ListItr对象,得到一个存放链表的集合(我在调试的时候,发现这个对象会做很多乱七八糟的工作,都跑到计算hash去了,具体的我就不太懂了)
    return new ListItr(index);
}

补充方法五:

private void checkPositionIndex(int index) {
    //如果不是正常范围的值,就抛出异常
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
    //如果传入的index节点大于等于0并且小于数组长度就返回true
    return index >= 0 && index <= size;
}

补充方法六:一定要区别于ArrayList对应迭代器的next方法

//该方法来自于AbstractList类的内部类ListItr下的方法
public E next() {
    //判断是否会发生并发修改异常
    checkForComodification();
    //判断下一个index节点是否小于链表长度,不小于则抛出异常
    if (!hasNext())
        throw new NoSuchElementException();
    //进行相应的赋值操作
    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}

由于随机访问的时候源码底层每次都需要进行折半的动作,再经过判断是从头还是从尾部一个个寻找。而顺序访问只会在获取迭代器的时候进行一次折半的动作,接下来获取元素都是在上一次的基础上获取下一个元素。因此顺序访问要比随机访问快得多



1.4、Cloneable接口类

一个类实现 Cloneable 接口来指示 Object.clone() 方法,该方法用于该类的实例进行字段的复制。在不实现 Cloneable 接口的实例上调用对象的克隆方法会导致 CloneNotSupportedException 异常。简言之克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝。

具体的运用(浅克隆、深克隆)可以查看之前的博客,原型模式



1.5、Serializable接口类

序列化由实现java.io.Serializable接口的类启用。 不实现此接口的类将不会使任何状态序列化或反序列化。 可序列化类的所有子类型都是可序列化的。 序列化接口没有方法或字段,类似于RandomAccess类

序列化:将对象的数据写入到文件(写对象)

反序列化:将文件中对象的数据读取出来(读对象)




2、ArrayList成员变量

明确并且牢记成员变量的含义,有助于对成员方法的理解

1、序列化版本号

private static final long serialVersionUID = 8683452581122892189L;

2、default_capacity(数组默认的容量)

private static final int DEFAULT_CAPACITY = 10;

3、empty_elementdata(用于空实例的共享空数组)

private static final Object[] EMPTY_ELEMENTDATA = {};

4、defaultcapacity_empty_elementdata(默认容量的空数组)

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

5、集合真正存储数组元素的数组

transient Object[] elementData;

6、集合的大小

private int size;




3、ArrayList构造方法

构造方法 方法详情
ArrayList<>() 无参构造,默认创建一个初始容量为十的空列表
ArrayList<>(int initialCapacity) 带参构造,创建一个指定初始容量的空列表
ArrayList<>(Collection<? extends E>c) 带参构造,创建一个包含指定集合的元素的列表

3.1、空参构造

初始化的集合是空集合

public ArrayList() {
    //由于没有指定集合大小,所以采用默认集合容量(空集合)进行赋值
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}



3.2、指定容量的带参构造

根据参数创建指定大小的结合

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        //如果指定集合容量大于0,跳转到Object类,进行创建指定大小的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //如果指定的容量是0,则创建一个空实例的共享空数组(这里的空数组不同于空参构造创建的空数组)
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //除此之外,抛出异常,比如负数
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}



3.3、以集合为参数的构造方法

将原集合的数据更新到新集合上

public ArrayList(Collection<? extends E> c) {
    //将集合构造方法中的集合对象转成数组,且将数组的地址赋值给elementData(真正存储数组元素的数组),toArray底层也是调用了补充方法七的方法
    elementData = c.toArray();
    
    //如果原集合的长度不等于0,就循环
    if ((size = elementData.length) != 0) {
        
        //判断elementData和Object[]是否为一样的类型
        if (elementData.getClass() != Object[].class)
            
            //如果不一样,就使用Arrays的copyOf方法进行元素的拷贝(具体方法详情,查看补充方法七)
            //三个参数分别为:原集合,原集合长度,Object类型
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        //如果原集合的长度等于0,就将空数组集合进行赋值
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

补充方法七:copyOf(将原集合拷贝到一个新的Object类型并且长度和原长度相同)

//三个参数分别为:原集合,原集合长度,Object类型
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    //使用镇压警告
    @SuppressWarnings("unchecked")
    
    //使用三元运算符进行赋值
    //判断传入的类型是否与Object类型相同
    T[] copy = ((Object)newType == (Object)Object[].class)
        //相同就返回一个新的Object类,并指定原来集合的长度
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    
    //将数组的内容拷贝到 copy 该数组中,arraycopy底层是native方法
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    //返回拷贝元素成功后的数组
    return copy;
}




4、ArrayList常用成员方法


4.1、传入指定元素的add()(第一次)

不指定集合容量,使用系统默认的10

public boolean add(E e) {
    //修改次数,该成员变量来自AbstractList类,默认初始值为0
    modCount++;
    //调用add方法,参数分别为:集合类型的数据,集合真正存储数据的集合,集合的大小
    add(e, elementData, size);
    //增加成功,返回一个true的布尔值
    return true;
}
//参数分别为:集合类型的数据,集合真正存储数据的集合,集合的大小
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        //如果集合的大小等于集合整整存储数据的集合大小,就进行扩容
        //如果我们使用的是无参构造,集合初始容量为默认的空基和,即长度为0.size默认也是0
        elementData = grow();
    //在返回的容量为10的数组中s位置,添加我们的传入的e元素
    elementData[s] = e;
    //集合的大小+1
    size = s + 1;
}
//调用具体的扩容方法
private Object[] grow() {
    //将集合的容量大小+1
    return grow(size + 1);
}
private Object[] grow(int minCapacity) {
    //此处的copyOf方法前文已经讲解过(注意参数含义的变化)
    //利用传入的参数,复制出一个新的数组,再赋值给真实的存储数据的集合(具体方法详情,查看补充方法八)
    return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
}

补充方法八:

//传入的参数为,size+1,即集合容量+1
private int newCapacity(int minCapacity) {
    //将原来真实集合的长度赋值给oldCapacity
    int oldCapacity = elementData.length;
    //将原集合的长度扩大1.5倍,再赋值给新的集合长度(第一次进来的时候为:0*1.5=0)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    //如果新的集合长度-旧的集合长度小于等于0
    if (newCapacity - minCapacity <= 0) {
       
        //判断集合的容量与默认的空数组集合是否相等
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            //返回传入参数和系统默认容量中的最大的值(第一次添加,最小容量为1,所以返回系统默认的容量)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        //如果传入的容量小于0,就抛出异常
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    //如果新的集合长度-旧的集合长度没有小于等于0 ,那就进行对应的运算
    //新的集合长度是否小于等于集合的最大长度。如果是就返回新的集合长度,如果不是,就返回集合的最大值或Integer的最大值(具体方法详情,查看补充方法九)
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}

补充方法九:

private static int hugeCapacity(int minCapacity) {
    //判断传入参数是否小于0,是就抛出异常
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //如果传入的参数大于集合的最大容量,就返回Integer的最大值,否则就返回集合的最大值
    return (minCapacity > MAX_ARRAY_SIZE)
        ? Integer.MAX_VALUE
        : MAX_ARRAY_SIZE;
}

一般的第二次添加数据时,不进行扩容直接进行赋值

private void add(E e, Object[] elementData, int s) {
    //此处的判断不成立,不进行扩容
    if (s == elementData.length)
        elementData = grow();
    //将需要添加的元素(此处传入的元素e)赋值在集合的s位置(s即为上一次的size)
    elementData[s] = e;
    size = s + 1;
}

集合在第一次添加数据的时候,系统会默认给集合10个空间的集合容量;

如果集合容量大于10以后,再进行扩容就是扩大为之前的1.5倍



4.2、传入指定位置和元素的add()

集合太小就扩容后添加到新的集合,否则直接添加

public void add(int index, E element) {
    //对传入的index值进行校验,如果大于集合长度或者小于0九抛出异常
    rangeCheckForAdd(index);
    //修改次数+1
    modCount++;
    final int s;
    //新建一个Object类型的数组
    Object[] elementData;
    
    //s = size 将集合末尾位置的下标值(不是集合的容量)赋值给s
    //(elementData = this.elementData).length 将我们的集合赋值给新创建的集合,再获取集合的长度
    //如果集合末尾位置的下标值与我们集合的长度相等,那么我们就进行扩容
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    
    //arraycopy底层使用了native关键字,调用方法将elementData数组的元素拷贝到新的elementData数组中
    System.arraycopy(elementData, index,elementData, index + 1, s - index);
    elementData[index] = element;
    //将集合大小+1
    size = s + 1;
}



4.3、传入的参数为一个ArrayList对象

集合容量比较后,进行相应的赋值

public boolean addAll(Collection<? extends E> c) {
    
    //调用toArray方法,调用底层的copyOf方法(该方法前文已经讲解了,补充方法七)
    //copyOf(将原集合拷贝到一个新的Object类型并且长度和原长度相同)
    Object[] a = c.toArray();
    
    //集合的修改次+1
    modCount++;
    
    //获取传入参数集合的长度,赋值给numNew参数
    int numNew = a.length;
    if (numNew == 0)
        //如果传入参数的为0则返回false
        return false;
    
    //新建一个Object数组
    Object[] elementData;
    final int s;
    
    //(elementData = this.elementData).length 将我们的集合赋值给新创建的集合,再获取集合的容量
    //如果传入集合的长度 > 我们集合的长度相等,那么我们就进行相应的扩容,扩容的长度
    if (numNew > (elementData = this.elementData).length - (s = size))
        elementData = grow(s + numNew);
    
    //arraycopy底层使用了native关键字,调用方法将a数组的元素拷贝到elementData数组中
    System.arraycopy(a, 0, elementData, s, numNew);
    //新的集合大小 = 传入的集合大小 + 之前的集合大小
    size = s + numNew;
    //方法执行成功,返回true
    return true;
}



4.4、传入的参数为一个ArrayList对象,且制定了具体位置

public boolean addAll(int index, Collection<? extends E> c) {
    //校验索引的正确性
    rangeCheckForAdd(index);

    //将旧集合中的元素拷贝到新的集合中
    Object[] a = c.toArray();
    //修改次数+1
    modCount++;
    //获取新集合的长度,再进行赋值
    int numNew = a.length;
    if (numNew == 0)
        //如果新集合的长度为0,即传入的集合为null,就结束方法,返回false
        return false;
    
    //创建一个存储元素的集合
    Object[] elementData;
    final int s;
    
    //(elementData = this.elementData).length 被添加的集合容量
    //如果传入参数的集合(添加的集合) > 被添加的集合容量 - 集合长度大小,就行行扩容
    if (numNew > (elementData = this.elementData).length - (s = size))
        elementData = grow(s + numNew);

    //计算移动的元素num
    int numMoved = s - index;
    if (numMoved > 0)
        //如果需要移动的元素大于0,则进行对应的
        //arraycopy底层使用了native关键字,调用方法将elementData数组的元素拷贝到elementData数组中
        System.arraycopy(elementData, index,
                         elementData, index + numNew,
                         numMoved);
    //如果不需要移动,就直接根据参数将a数组的元素拷贝到elementData数组中
    System.arraycopy(a, 0, elementData, index, numNew);
    
    //集合的大小进行相应的添加
    size = s + numNew;
    //方法执行成功,返回true
    return true;
}

对于add()方法,重点学习我们的传入指定元素方法,后面的就一通百通了,因为很多的方法、步骤都相同



4.5、根据索引对元素进行删除

找到对应索引,赋值为null,让垃圾回收器回收

public E remove(int index) {
    //对传入的索引值进行校验(方法详情,请查看补充方法十)
    Objects.checkIndex(index, size);
    //将集合真正存储元素的数组赋值给一个Object类型的数组
    final Object[] es = elementData;

    //镇压警告
    //将对应索引的元素赋值给oldValue,方便后期返回
    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    
    //根据索引和新创建的数组集合(数据来自原集合)删除指定位置的元素(方法详情,请查看补充方法十一)
    fastRemove(es, index);

    //返回被移除的元素的元素
    return oldValue;
}

补充方法十:

public static int checkIndex(int index, int length) {
    return Preconditions.checkIndex(index, length, null);
}
//正真校验的方法体
public static <X extends RuntimeException> int checkIndex(int index, int length,
               BiFunction<String, List<Integer>, X> oobef) {
    if (index < 0 || index >= length)
        //如果索引值小于0,或者索引值大于集合的大小就抛出异常
        throw outOfBoundsCheckIndex(oobef, index, length);
    return index;
}

补充方法十一:

private void fastRemove(Object[] es, int i) {
    //修改次数+1
    modCount++;
    final int newSize;
    
    //size - 1 数组的下标为0开始,-1运算后表示与数组下标同步
    //如果集合的数据容量 > 我们指定的索引,即为查找到了需要移除的元素
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    
    //将原集合删除元素的位 置为null,尽早让垃圾回收机制对其进行回收
    es[size = newSize] = null;
}



4.6、根据指定元素进行删除

根据元素,找到对饮的索引,再使用同4.5一样的删除方式

public boolean remove(Object o) {
    //将集合真正存储元素的数组赋值给一个Object类型的数组
    final Object[] es = elementData;
    //获取调用对象集合的大小
    final int size = this.size;
    int i = 0;
    
    //此处使用了label语法,用来方便跳出循环
    //此代码块是为了找到对应的元素
    found: {
        if (o == null) {
            //如果对象为null,就从小到大的遍历,删除null,再跳出循环
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            //如果元素不为null,就从小到大的遍历集合,使用equals判断元素是否相等,再跳出循环
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        //如果都没有找到,就返回false
        return false;
    }
    //根据刚才遍历的结果,获得对应的索引值,然后再次使用删除方法,进行删除元素(即按照元素删除只是在按照索引删除前多一个内容查找)
    fastRemove(es, i);
    return true;
}



4.7、修改集合元素

找到对应的索引,直接修改

//传入需要修改的元素下标,以及需要修改后的内容
public E set(int index, E element) {
    //校验传入下标是否合理
    Objects.checkIndex(index, size);
    
    //获取对应下标的内容,并且赋值给oldValue变量(方法详情,请查看补充方法十二)
    E oldValue = elementData(index);
    
    //将修改后的内容赋值到对应下标的内容上
    elementData[index] = element;
    
    //返回修改之前的值
    return oldValue;
}

补充方法十二:

//根绝对应的索引,返回对应索引上的内容
E elementData(int index) {
    return (E) elementData[index];
}



4.8、获取指定的元素

找到索引,直接返回

public E get(int index) {
    //检验传入的索引是否符合规范
    Objects.checkIndex(index, size);
    //返回对应位置上的元素(方法详情,请查看补充方法十二)
    return elementData(index);
}



4.9、toString方法

ArrayList集合打印时,自带toString方法

测试代码:

package pers.mobian;

import java.util.ArrayList;

public class arraylist01 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("a1");
        list.add("a2");
        list.add("a3");
        list.add("a4");
        System.out.println(list);

        if (list instanceof ArrayList) {
            System.out.println("我是集合类型");
        }
        System.out.println("===============");

        String s1 = list.toString();
        System.out.println(s1);

        if (s1 instanceof String) {
            System.out.println("我是字符串类型");
        }
    }
}

执行结果:

[a1, a2, a3, a4]
我是集合类型
===============
[a1, a2, a3, a4]
我是字符串类型

我们不难发现,无论是字符串类型还是集合类型,他们的长相都是一样的。toString方法我们能理解,饭是ArrayList为什么呢???

我们再次进入源码

public void println(Object x) {
    String s = String.valueOf(x);
    //加了把锁
    synchronized (this) {
        print(s);
        //换行
        newLine();
    }
}
public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}
public String toString() {
    //获取对应的迭代器对象
    Iterator<E> it = iterator();
    //判断元素是否为null,如果为null直接返回[]
    if (! it.hasNext())
        return "[]";

    //创建一个StringBuilder类的对象
    StringBuilder sb = new StringBuilder();
    sb.append('[');
    //遍历迭代器中的元素,然后不断地追加元素
    for (;;) {
        //获取迭代器对象的元素赋值给e(方法详情,请查看补充方法十三)
        E e = it.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! it.hasNext())
            //返回时调用了toString方法
            return sb.append(']').toString();
        sb.append(',').append(' ');
    }
}

补充方法十三:该方法是ArrayList获取的迭代器对象的next方法,与LinkList下的next方法有区别(此处也可以看出两种集合结构的区别)

//获取迭代器对象的下一个元素
public E next() {
    //检查修改的次数,如果修改的次数和预期的修改次数不一样,就会抛出异常
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    //将旧的集合赋值到新创建的Object数组中
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    //返回刚刚经过的元素
    return (E) elementData[lastRet = i];
}

我们查看源码发现打印输出集合的时候,系统会执行对应的toString方法,即我们看到的集合形式和我们用toString方法打印的相同



4.10、获取迭代器的方法

该方法在分析LinkList使用迭代器遍历的时候已经讲解过,有兴趣的拉到开头去瞅瞅

至此,ArrayList的增删改查方法的执行流程已经讲解完毕,在面试中一般是结合LinkList进行考察。原理部分的总结可以参考我的其他博客
传送门:
ArrayList原理讲解

发布了45 篇原创文章 · 获赞 17 · 访问量 3689

猜你喜欢

转载自blog.csdn.net/qq_44377709/article/details/105273147
今日推荐