源码那些事超详细的ArrayList底层源码+经典面试题

Posted 温文艾尔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码那些事超详细的ArrayList底层源码+经典面试题相关的知识,希望对你有一定的参考价值。

🙏相见即是有缘,如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ☺

👋ArrayList底层源码

ArrayList的类图示:

  1. ArrayList集合介绍
    1. List底层基于动态数组实现
  2. 数组结构介绍
    • 增删慢:每次删除元素都需要更改数组长度,拷贝以及移动元素位置
    • 查询快:由于数组在内存中是一块连续空间,因此可以根据地址+索引的方式快速获取对应位置上的元素

👋1 ArrayList继承关系

Serializable标记性接口

介绍 类的序列化由实现java.io.Serializable接口的类启用。不实现此接口的类将不会使任何状态序列化反序列化。可序列化类的所有子类型都是可序列化的,序列化接口没有方法或字段,仅用于标识可串行化的语义

  1. 序列化:将对象的数据写入到文件(写对象)
  2. 反序列化:将文件中对象的数据读取出来(读对象)

Cloneable标记性接口

  1. 介绍 一个类实现Cloneable接口来指示Object.clone()方法,该方法对于该类的实例进行字段的复制是合法的。在不实现Cloneable接口的实例上调用对象的克隆方法会导致异常CloneNotSupportedException被抛出。简言之:克隆就是依据已有的数据,创造一份新的完全一样的数据拷贝
  2. 克隆的前提条件
    • 被克隆对象所在的类必须实现Cloneable接口
    • 必须重写clone()方法

Serializable接口

  1. 由List实现使用,以表明它们支持快速(通常为恒定时间)随机访问
  2. 此接口的主要目的是允许算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能

👋1.1 Cloneable的底层实现

    /**
     * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
     * elements themselves are not copied.)
     *
     * @return a clone of this <tt>ArrayList</tt> instance
     */
    public Object clone() 
        try 
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
         catch (CloneNotSupportedException e) 
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        
    
可以看到通过调用clone()方法,返回了一个ArrayList对象
 v.elementData = Arrays.copyOf(elementData, size);

是我们能完成复制的关键,点进去

    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;
    

我们原来数组中的数据通过System.arraycopy方法被拷贝到copy数组中,并将这个数组返回

克隆牵扯到浅拷贝和深拷贝,这里简述一下浅拷贝深拷贝的区别

首先创建一个Student类

public class Student implements Cloneable 
    private String name;
    private Integer age;
    public Student() 
    
    public Student(String name, Integer age) 
        this.name = name;
        this.age = age;
    
    public String getName() 
        return name;
    
    public void setName(String name) 
        this.name = name;
    
    public Integer getAge() 
        return age;
    
    public void setAge(Integer age) 
        this.age = age;
    
    @Override
    public Object clone() throws CloneNotSupportedException 
        return super.clone();
    
    @Override
    public String toString() 
        return "Student" +
                "name='" + name + '\\'' +
                ", age=" + age +
                '';
    

浅拷贝实例

package ArrayListProject.CloneTest;

import java.util.ArrayList;
import java.util.List;

/**
 * Description
 * User:
 * Date:
 * Time:
 */
public class Test 
    public static void main(String[] args) 
        ArrayList list = new ArrayList();
        Student stu = new Student("zs",18);
        list.add("123");
        list.add(stu);
        System.out.println("未拷贝之前的数组:"+list);
        ArrayList clone = (ArrayList)list.clone();
        System.out.println("拷贝之后的数组:"+clone);
        System.out.println("两个数组地址是否相等:"+(list==clone));
        System.out.println("两个数组中的stu对象是否相等:");
        System.out.println("list中的stu对象:"+list.get(1));
        System.out.println("修改stu对象之前,clone中的stu对象:"+clone.get(1));
        Student stu1 = (Student)list.get(1);
        stu1.setName("ls");
        System.out.println("修改stu对象之后,clone中的stu对象:"+clone.get(1));
        System.out.println("两者stu对象的地址是否相同"+(list.get(1)==clone.get(1)));
    


未拷贝之前的数组:[123, Studentname='zs', age=18]
拷贝之后的数组:[123, Studentname='zs', age=18]
两个数组地址是否相等:false
两个数组中的stu对象是否相等:
list中的stu对象:Studentname='zs', age=18
修改stu对象之前,clone中的stu对象:Studentname='zs', age=18
修改stu对象之后,clone中的stu对象:Studentname='ls', age=18
两者stu对象的地址是否相同true

由此我们得出

  • 克隆之后的对象是一个新对象,list==clone为false可知二者地址不相同
  • 由list.get(1)==clone.get(1)为true可知,两者中的stu对象为同一对象
  • 修改list中的stu对象,clone中的stu对象也会随之改变可知,克隆的是stu对象的地址,并没有创建新的对象,它仅仅是拷贝了一份引用地址
  • 基本数据类型可以达到完全复制,而引用数据类型则不可以

深拷贝和浅拷贝不同,深拷贝中拷贝的对象是一个完全新的对象,他拷贝的并不是引用地址,而是实实在在的创建了一个对象

👋1.2 RandomAccess标记接口

  • 由List实现使用,以表明它们支持快速(通常为恒定时间)随机访问
  • 此接口的主要目的是允许算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能

因此List实现RandomAccess在执行随机访问时,性能会比顺序访问更快

public interface RandomAccess 

我们通过一个案例来证明

我们以1000000个数据作为测试

package ArrayListProject.CloneTest;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test02 
    public static void main(String[] args) 
        //创建ArrayList集合
        List<String> list = new ArrayList<>();
        //添加100W条数据
        for (int i = 0; i < 1000000; i++) 
            list.add(i+"a");
        

        //测试随机访问时间
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) 
            //取出集合的每一个元素
            list.get(i);
        
        long endTime = System.currentTimeMillis();
        System.out.println("随机访问耗时:"+(endTime-startTime));
        //测试顺序访问时间
        long startTime2 = System.currentTimeMillis();
        Iterator<String> it = list.iterator();
        while (it.hasNext())
            it.next();
        
        long endTime2 = System.currentTimeMillis();
        System.out.println("顺序访问耗时:"+(endTime2-startTime2));
    


随机访问耗时:3
顺序访问耗时:4
由此可见,在数据量极大的情况下,ArrayList随机访问的效率远高于顺序访问

而LinkedList的数据结构是链表,且未实现RandomAccess接口,他的效率和ArrayList相比如何呢,我们来做一个
测试
package ArrayListProject.CloneTest;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class Test03 
    public static void main(String[] args) 
        List<String> list1 = new LinkedList<>();
        for (int i = 0; i < 100000; i++) 
            list1.add(i+"a");
        

        //测试随机访问时间
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < list1.size(); i++) 
            //取出集合的每一个元素
            list1.get(i);
        
        long endTime = System.currentTimeMillis();
        System.out.println("随机访问耗时:"+(endTime-startTime));
        //测试顺序访问时间
        long startTime2 = System.currentTimeMillis();
        Iterator<String> it = list1.iterator();
        while (it.hasNext())
            it.next();
        
        long endTime2 = System.currentTimeMillis();
        System.out.println("顺序访问耗时:"+(endTime2-startTime2));
    

随机访问耗时:11676
顺序访问耗时:4

由此可见,没有实现RandomAccess接口的LinkedList集合的随机访问速度远远小于顺序访问

👋2 ArrayList构造方法源码解析

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

👋2.1 ArrayList()构造方法

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    //构造一个初始容量为10的数组
    //DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认的空容量的数组
    //elementData:集合真正存储数据的容器
    public ArrayList() 
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    

👋2.2 ArrayList(int initialCapacity)构造方法

    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public ArrayList(int initialCapacity) 
        if (initialCapacity > 0) 
            //如果传进来的变量大于0,则初始化一个指定容量的空数组
            this.elementData = new Object[initialCapacity];
         else if (initialCapacity == 0) 
            //传进来的变量=0,则不去创建新的数组,直接将已创建的EMPTY_ELEMENTDATA空数组传给
            //ArrayList
            this.elementData = EMPTY_ELEMENTDATA;
         else 
            //传进来的指定数组容量不能<0
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        
    

👋2.3 ArrayList(Collection<? extends E> c)构造方法

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) 
        //将构造方法中的参数转换成数组形式,其底层是调用了System.arraycopy()
        elementData = c.toArray();
        //将数组的长度赋值给size
        if ((size = elementData.length) != 0) 
            //检查elementData是不是Object[]类型,不是的话将其转换成Object[].class类型
            if (elementData.getClass() != Object[].class)
                //数组的创建与拷贝
                elementData = Arrays.copyOf(elementData, size, Object[].class);
         else 
            //size为0,则把已创建好的空数组直接给它
            this.elementData = EMPTY_ELEMENTDATA;
        
    

👋3 ArrayList基础方法

👋3.05 ArrayList中的各个变量

//长度为0的空数组
private static final Object[] EMPTY_ELEMENTDATA = ;

//默认容量为0的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ;

//集合存元素的数组
transient Object[] elementData;

//集合的长度
private int size;

//集合默认容量
private static final int DEFAULT_CAPACITY = 10;

👋3.1添加方法

方法名描述
add(E e)将指定的元素追加到此列表的末尾
add(int index, E element)在此列表中的指定位置插入指定的元素
addAll(Collection<? extends E> c)按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾
addAll(int index, Collection<? extends E> c)将指定集合中的所有元素插入到此列表中,从指定的位置开始

👋3.1.1 add(E e)

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by @link Collection#add)
     */
    //将指定的元素追加到此列表的末尾
    public boolean add(E e) 
        //每增加1个元素,数组所需容量+1,并检查增加数组容量后是否要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //添加元素
        elementData[size++] = e;
        return true;
    


    private void ensureCapacityInternal(int minCapacity) 
        //如果是默认的DE

以上是关于源码那些事超详细的ArrayList底层源码+经典面试题的主要内容,如果未能解决你的问题,请参考以下文章

ArrayList 源码详细分析

ArrayList 源码分析

JAVA——底层源码阅读——集合ArrayList的实现底层源码分析

ArrayList详解

2018.9.7 ArrayList

ArrayList底层源码实现练习