ArrayList底层源码分析

    技术2022-07-14  94

    声明:本文为作者原创,请勿装载,如过转载,请注明转载地址

    文章目录

    ArrayList底层源码分析1. 继承Serializable接口2. 继承Cloneable接口2.1 浅拷贝2.2 深拷贝 3. RandomAccess接口4. ArrayList源码分析4.1 数据域4.2 构造函数4.3 在数组末尾添加一个元素4.3.1 add(E e) 向数组末尾添加一个元素4.3.2 ensureCapacityInternal(int minCapacity)保证存在剩余空间存放要添加的元素4.3.3 grow(int minCapacity)扩容机制 4.4 add(int index, E element)方法4.5 set(int index, E element) 方法4.6 get(int index)4.7 remove(int index)

    ArrayList底层源码分析

    (1) ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,不像数组一旦初始化长度就不可被改变

    (2) ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。

    (3) ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。

    增删慢:每次删除元素,都需要更改数组的长度,拷贝以及移动元素的位置

    查询快:由于数组在内存中是一块连续的空间,因此可以根据索引快速获取某个位置上的元素。

    1. 继承Serializable接口

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

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

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

    //Serializable 源码,接口中并没有方法 public interface Serializable { }

    2. 继承Cloneable接口

    一个类实现Cloneable接口来指示Object.clone()方法,该方法对于该类的实例进行字段的复制是合法的,如果不实现该接口,那么调用clone()方法户抛出异常。克隆就是根据已有的数据创造一份新的完全一样的数据拷贝。

    // Cloneable 源码,接口中并没有方法 public interface Cloneable { }

    克隆的前提:被克隆对象所在的类必须实现Cloneable接口,必须重写clone()方法。

    public class CloneDemo { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); //调用方法进行克隆 Object obj = list.clone(); System.out.println(obj==list); //false System.out.println(obj); //[aaa, bbb, ccc] System.out.println(list); //[aaa, bbb, ccc] } }

    2.1 浅拷贝

    //Student类 @Data public class Student implements Cloneable { private String name; private int age; private Skill skill; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } //省略构造函数、toString()方法 } //Skill类 @Data public class Skill { private String skillName; //省略构造函数、toString()方法 } //CloneDemo1测试类 public class CloneDemo1 { public static void main(String[] args) throws CloneNotSupportedException { ArrayList<String> list = new ArrayList<>(); Skill skill = new Skill("弹钢琴"); Student stu1 = new Student("小明",18,skill); //调用clone()方法阿进行数据的拷贝 Object stu2 = stu1.clone(); System.out.println(stu1); System.out.println(stu2); //修改stu1的年龄(修改基本类型的值) stu1.setAge(30); //修改技能(修改引用类型的值) skill.setSkillName("画画"); System.out.println(stu1); System.out.println(stu2); } }

    结果:

    Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=30, skill=Skill{skillName='画画'}} Student{name='小明', age=18, skill=Skill{skillName='画画'}}

    存在的问题:基本数据类型可以达到完全复制,但是引用数据类型不可以

    原因:在学生对象stu1被拷贝时,其属性skill(引用数据类型)仅仅是拷贝了一份引用,因此当被克隆对象stu1的skill的值发生改变时,克隆对象stu2的属性skill也将跟着改变。

    如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址。

    (1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。 (2) 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。

    2.2 深拷贝

    //Skill类,实现Cloneable接口 @Data public class Skill implements Cloneable { private String skillName; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } //Student类 @Data public class Student implements Cloneable { private String name; private int age; private Skill skill; @Override protected Object clone() throws CloneNotSupportedException { //深拷贝不能简单的调用父类的方法 //先克隆一个学生对象 Student stu = (Student)super.clone(); //再克隆一个skill对象 Skill skill = (Skill) stu.skill.clone(); //将克隆出来的skill对象赋值给stu对象的成员变量 stu.setSkill(skill); return stu; } //省略... } //测试类不变

    结果:

    Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=30, skill=Skill{skillName='画画'}} Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}}

    深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

    对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。 (2) 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。 (3) 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。 (4) 深拷贝相比于浅拷贝速度较慢并且花销较大。

    深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。

    3. RandomAccess接口

    RandomAccess接口这个空架子的存在,是为了能够更好地判断集合是否ArrayList或者LinkedList,从而能够更好选择更优的遍历方式,提高性能!

    4. ArrayList源码分析

    4.1 数据域

    public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ //数组使用空参数构造函数初始化时,第一次添加元素后的初始化容量 private static final int DEFAULT_CAPACITY = 10; //一个空数组,用于带参构造函数的初始化 private static final Object[] EMPTY_ELEMENTDATA = {}; //一个空数组,用于空参构造函数初始化 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //当前数据对象存放地方 transient Object[] elementData; // non-private to simplify nested class access //当前数组中元素的个数 private int size; //数组最大可分配的容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 集合数组修改次数的标识(由AbstractList继承下来)(fail-fast机制) protected transient int modCount = 0; }

    4.2 构造函数

    /** * 1、空参数构造函数 */ public ArrayList() { //让的elelmentData指向一个空数组,这个数组的容量为0,第一次调用add()方法时会初始化为10 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 2、创建指定长度的构造函数 */ public ArrayList(int initialCapacity) { //如果传入的容量的大于0,就创建一个指定容量的空数组用来存储元素 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; //创建一个长度为0的空数组,这个数组的容量为0,第一次调用add()方法时会初始化为1 } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); } } /** * 3、构造一个包含指定元素的列表的列表list,按照集合的顺序返回迭代器。 */ public ArrayList(Collection<? extends E> c) { //将传入的集合转化为数组并浅拷贝给elementData elementData = c.toArray(); //将转换后的数组长度赋值给size,并判断是否为0 if ((size = elementData.length) != 0) { //数组的创建和拷贝 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 用EMPTY_ELEMENTDATA替换空数组,在add()时,容量就会变成1 this.elementData = EMPTY_ELEMENTDATA; } }

    **jdk8:**ArrayList中维护了Object[] elementData,初始容量为0,第一次添加时,将初始elementData的容量为10再次添加时,如果容量足够,则不用扩容直接将新元素赋值到第一个空位上,如果容量不够,会扩容1.5倍

    **jdk7:**ArrayList中维护了Object[] elementData,初始容量为10.添加时,如果容量足够,则不用扩容直接将新元素赋值到第一个空位上。如果容量不够,会扩容1.5倍

    jdk7 相当于饿汉式,创建对象时,则初始容量为10 jdk8 相当于懒汉式,创建对象时,并没有初始容量为10,而在添加时才去初始容量为10

    4.3 在数组末尾添加一个元素

    4.3.1 add(E e) 向数组末尾添加一个元素

    // 在数组的结尾添加一个元素 public boolean add(E e) { // 确保数组已使用长度(size)加1之后足够存下 下一个数据 ensureCapacityInternal(size + 1); // 数组的下一个位置存放传入元素,同时将size后移 elementData[size++] = e; //始终返回true return true; }

    4.3.2 ensureCapacityInternal(int minCapacity)保证存在剩余空间存放要添加的元素

    /** 1、如果调用的是空参数构造函数:比较初始容量和所需最小容量的大小,并返回较大的 2、如果调用的带参数构造函数:直接返回最小容量(size+1) **/ private static int calculateCapacity(Object[] elementData, int minCapacity) { /** 如果ArrayList创建对象时调用的是空参数构造函数: 第一次调用add()方法添加元素时:会在这儿将数组的容量初始化为10 随后调用add()方法添加元素时:会比较添加该元素所需的最小容量minCapacity和10的大小,返回大的 */ if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } /** 如果ArrayList创建对象时调用的为带参构造函数:直接返回最小容量minCapacity = size+1 */ return minCapacity; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { modCount++; //如果 minCapacity(即size+1当前需要的最小容量) > elementData.length(数组的容量),需要扩容 //使得存在剩余的数内存存放要添加的元素 if (minCapacity - elementData.length > 0) grow(minCapacity); }

    4.3.3 grow(int minCapacity)扩容机制

    ArrayList的扩容机制:

    private void grow(int minCapacity) { // 获取数组容量 int oldCapacity = elementData.length; // 扩容1.5倍(将数组的容量扩容1.5倍) int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩展为1.5倍后还不满足需求,则直接扩展为需求值 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 检查扩容后的容量是否大于最大容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 申请一块更大内存容量的数组,将原来数组中的元素挪到这个新数组中,同时将elementData指向这个新数组 // 原来的旧的数组由于没有引用变量指向他,就会被垃圾回收机制回收掉 elementData = Arrays.copyOf(elementData, newCapacity); }

    总结:在数组的末尾添加一个元素:

    (1) 调用ensureCapacityInternal(size + 1);方法,该方法的作用是确保数组的长度加1后内存是充足的,就是说保证能够数组还能存放一个元素。

    (2) 调用calculateCapacity(elementData, minCapacity)计算添加一个元素所需的最小容量minCapacity;根据ArrayList创建对象时使用的构造方法分为两种情况:

    如果使用空参数构造函数创建ArrayList对象,会在第一次调用add()方法添加元素时,将数组的容量初始化为10,随后每次调用add()方法添加元素时都会比较初始容量10与minCapacity的大小,并返回大的容量。

    如果使用带参构造函数创建ArrayList对象,会直接但是minCapacity。

    (3) 调用ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));该方法就是ensureCapacityInternal(size + 1)方法的底层,其作用是确保数组的长度加1后内存是充足的,就是说保证能够数组还能存放一个元素。

    若添加一个元素所需的最小容量minCapacity大于数组的长度if (minCapacity - elementData.length > 0)那么就需要扩容,此时就会调用扩容方法:grow(int minCapacity)

    (4) 调用 grow(int minCapacity)方法,该方法会对数组容量进行扩容。

    在扩容时会申请一块更大内存容量的数组,将原来数组中的元素拷贝到这个新的数组中,同时让elementData指向该数组,而原来的数组由于没有新的指针指向它,就会被垃圾回收机制回收。

    在扩容时,首先考虑扩容1.5倍(如果扩容的太大,可能会浪费更多的内存,如果扩容的太小,又会导致频繁扩容,申请数组拷贝元素等对添加元素的性能消耗较大,因此1.5倍刚好)。如果扩容1.5倍还是内存不足,就会直接扩容到添加元素所需的最小的容量。同时不能超过数组的最大容量Integer.MAX_VALUE - 8。

    4.4 add(int index, E element)方法

    先数组的指定位置添加元素:

    public void add(int index, E element) { //判断索引是否越界 rangeCheckForAdd(index); //确保数组size+1之后能够存下 下一个数据 ensureCapacityInternal(size + 1); /** 源数组 elementData 从传入形参index处开始复制,复制size-index个元素 目标数组 elementData 从index+1处开始粘贴,粘贴从源数组赋值的元素 **/ System.arraycopy(elementData, index, elementData, index + 1,size - index); //把index处的元素替换成新的元素。 elementData[index] = element; //将size后移 size++; } //判断索引是否越界 private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } // 这是一个本地方法,由C语言实现。 public static native void arraycopy(Object src, // 源数组 int srcPos, // 源数组要复制的起始位置 Object dest, // 目标数组(将原数组复制到目标数组) int destPos, // 目标数组起始位置 int length // 复制源数组的长度 );

    总结:在数组的指定位置添加元素

    (1) 调用rangeCheckForAdd(index)方法判断输入的索引是否越界

    (2) 调用ensureCapacityInternal(size + 1)方法确保数组的容量充足,至少保证能再装下一个元素。

    (3) 调用System.arraycopy(elementData, index, elementData, index + 1,size - index);方法,这是C语言的一个实现方法,这个方法的作用是复制elementData数组中(index,size-index)区间内的元素,粘贴到elementData数组中(index+1,size-index+1)的位置处。

    从图中可看出,表现出的结果就是将elementData数组中的元素,从要插入的位置index开始向后移动一个位置,给要插入的元素腾出一个位置,将该元素插入到index位置处。

    4.5 set(int index, E element) 方法

    设置index位置处的元素

    public E set(int index, E element) { rangeCheck(index); //记录index位置处的旧值 E oldValue = elementData(index); //将数组index索引处的元素设置为新值element elementData[index] = element; //返回旧值 return oldValue; } //判断索引越界 private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }

    4.6 get(int index)

    获取指定索引位置处的元素

    public E get(int index) { //判断索引越界 rangeCheck(index); //返回指定索引位置处的元素 return elementData(index); }

    因为数组的内存是连续的,因此可以根据索引直接获取元素。

    4.7 remove(int index)

    //删除该列表中指定位置的元素,将所有后续元素转移到前边(将其中一个元素从它们中减去指数) public E remove(int index) { //判断索引越界 rangeCheck(index); modCount++; //获取index位置处的旧值 E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) /** 源数组 elementData 从传入形参index+1处开始复制,复制size-index-1个元素 目标数组 elementData 从index处开始粘贴,粘贴从源数组赋值的元素 */ System.arraycopy(elementData, index+1, elementData, index, numMoved); //将size减1指向最后一个元素位置,再将最后一个位置清空 elementData[--size] = null; return oldValue; } // 这是一个本地方法,由C语言实现。 public static native void arraycopy(Object src, // 源数组 int srcPos, // 源数组要复制的起始位置 Object dest, // 目标数组(将原数组复制到目标数组) int destPos, // 目标数组起始位置 int length // 复制源数组的长度 );

    总结:

    (1) 调用rangeCheckForAdd(index)方法判断输入的索引是否越界

    (3) 调用System.arraycopy(elementData, index+1, elementData, index, size - index - 1);该方法的作用是复制elementData数组中(index+1,size-index-1)区间内的元素,粘贴到elementData数组中的(index,size-index-2)位置处。

    从图中可看出,表现出的结果就是将elementData数组中的元素,从要删除的元素的后面一个位置开始到末尾的元素结束都向前移动一个位置,最后将size–,将最后一个元素置为null

    Processed: 0.010, SQL: 9