ArrayList源码简析
创始人
2024-03-31 16:43:46
0

ArrayList源码简析

  • ArrayList 简介
    • Arraylist 和 Vector 的区别?
    • Arraylist 与 LinkedList 区别?
  • 设计思路
    • 初始化
    • 插入元素
    • 扩容检查
    • ensureCapacity
    • System.arraycopy() 和 Arrays.copyOf()方法
  • 迭代器


ArrayList 简介

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。

public class ArrayList extends AbstractListimplements List, RandomAccess, Cloneable, java.io.Serializable{}
  • RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了 Cloneable 接口 ,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现了 java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

Arraylist 和 Vector 的区别?

  1. ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  2. VectorList 的古老实现类,底层使用 Object[ ]存储,线程安全的。

Arraylist 与 LinkedList 区别?

先来说说两者的相同点吧:

  • 非线程安全

两者的不同点其实就是数组和链表的区别,这里不详细进行展开了,只是概述一番:

  • 底层数据结构不同
  • 查询,插入和删除元素复杂度不同
  • 内存空间占用不同

设计思路

ArrayList本质是一个动态数组的实现,如果要设计一个动态数组,我们需要考虑哪些方面呢?

  • 扩容 ! ! ! (扩容是动态数组是否高效的核心)
  • 数组默认大小,应该提供接口让用户能够按照业务需求规定初始动态数组的大小,这样可以减少频繁扩容带来的性能损耗。

数组的增删查改没有什么独特优化技巧,无法就是需要在插入前进行扩容判断而已。

ArrayList的核心属性也就是下面两个:

    //底层动态数组transient Object[] elementData;//动态数组内部元素数量private int size;

动态数组的长度不等于动态数组里面元素的数量,动态数组的长度称为容量,通常都是容量大于元素数量的。


初始化

ArrayList为我们提供了两种初始化方式,一种是由用户自定规定默认初始化的数组大小,另一种是初始化一个默认大小的数组:

    public ArrayList(int initialCapacity) {if (initialCapacity > 0) {//直接初始化一个大小为用户指定大小的数组(这里并没有采用懒加载)this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {//说明用户要初始化一个空数组--对于空数组的表示,ArrayList采用了一个共享空数组变量实例来表示//实际插入元素时,才会扩容初始化(懒加载)this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}public ArrayList() {//采用默认初始化方案,初始化一个默认大小的数组,这里也是采用了一个共享实例进行标记//实际是等到真正插入元素的时候,才会进行初始化(懒加载)this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}

这两个共享实例的类型虽说一致,但是含义却不同,核心作用都是进行标记,为了懒加载服务。

    private static final Object[] EMPTY_ELEMENTDATA = {};private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

插入元素

插入元素分为两种情况:

  • 直接插入到数组尾部
  • 插入到指定索引位置处
    public boolean add(E e) {//扩容检查ensureCapacityInternal(size + 1); //插入数组尾部elementData[size++] = e;return true;}public void add(int index, E element) {//检查Index是否合法--这里大家自行查看源码即可rangeCheckForAdd(index);//扩容检查ensureCapacityInternal(size + 1); //插入到数组指定位置处,这里需要将index开始的元素都后移一位,然后在index位置处插入当前元素System.arraycopy(elementData, index, elementData, index + 1,size - index);            elementData[index] = element;size++;}

插入元素前需要先进行扩容检查,我们下面来看看。


扩容检查

  • 确保当前数组大小能够再塞下n个元素
    //这里minCapacity=size+n,add方法传入的n=1,而addAll方法传入的n就不确定是多大了private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}
  • 处理采用默认大小数组情况下的懒加载问题
    private static int calculateCapacity(Object[] elementData, int minCapacity) {//如果动态数组初始化的时候采用的是默认大小,然后构造方法只是打了个标记,还没有进行初始化//如果下面这个条件满足,说明此时应该是第一次调用add或者addAll方法,size此时等于0//而minCapacity的大小此时就是等于n,如果调用add方法,那么n=1,否则n是不确定的if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果调用的是addAll方法,并且n大于10,那么进行真正初始化的时候,数组大小采用n的大小//否则在n<10的情况,还是采用默认大小10return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}
  • 判断当前数组是否可以再放下n个元素,如果可以就不进行扩容
    private void ensureExplicitCapacity(int minCapacity) {modCount++;//minCapacity=size+n//length=size+数组剩余空闲容量//下面这个等式等价于: n>数组剩余空闲容量if (minCapacity - elementData.length > 0)//扩容grow(minCapacity);}
  • 扩容
    private void grow(int minCapacity) {//拿到当前数组的容量int oldCapacity = elementData.length;//当前数组容器的1.5倍作为新容量int newCapacity = oldCapacity + (oldCapacity >> 1);//判断上面通过默认1.5倍扩容方式得到的新容量是否满足当前再插入n个元素的需求//minCapacity=size+n   newCapacity=size+剩余空闲容量大小,因为扩容了,所以空闲容量大小更大了if (newCapacity - minCapacity < 0)//如果不满足,一般都是调用了addAll方法,此时新容量就等于minCapacity大小newCapacity = minCapacity;//如果数组元素过多直接超过最大限制,那么需要进行处理//这部分内容比较简单,大家就自行看看源码吧    if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);//拿到扩容后的新数组elementData = Arrays.copyOf(elementData, newCapacity);}

ensureCapacity

ensureCapacity方法是ArrayList提供的方法,旨在插入大量元素前,用户通过调用该方法提前扩容到对应的大小,避免插入过程中频繁扩容。

    public void ensureCapacity(int minCapacity) {//如果当前数组采用默认大小进行初始化的,那么当前用户指定的扩容大小必须要大于默认大小,否则没必要扩容int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)? 0: DEFAULT_CAPACITY;if (minCapacity > minExpand) {//该方法内部还会再次判断,如果用户指定的扩容大小小于当前数组的容量,那么也没有必要进行扩容操作。ensureExplicitCapacity(minCapacity);}}

System.arraycopy() 和 Arrays.copyOf()方法

ArrayList底层的add,remove,grow等涉及到对底层数组具体的操作,都是由上述工具类提供的API来完成的,下面简单介绍一下ArrayList中用到的相关API:

  • System.arraycopy() 方法
    // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义/***   复制数组* @param src 源数组* @param srcPos 源数组中的起始位置* @param dest 目标数组* @param destPos 目标数组中的起始位置* @param length 要复制的数组元素的数量*/public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos,int length);
  • Arrays.copyOf()方法
    public static int[] copyOf(int[] original, int newLength) {// 申请一个新的数组int[] copy = new int[newLength];// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;}

Arrays.copyOf()方法主要是为了给原有数组扩容


迭代器

ArrayList本身的源码并没有太多难点,但是容易被大家忽略的是ArrayList内部提供的Iterator,就是因为这个Iterator,才有了令很多人头疼的ConcurrentModificationException并发修改异常出现,下面就来看看Itr迭代器是如何实现的:

private class Itr implements Iterator {//下一个要访问的元素下标---如果构造Itr的时候不传入,默认为0int cursor;      //上一个要访问的元素下标int lastRet = -1; //代表对ArrayList修改次数的期望值,初始值为modCountint expectedModCount = modCount;Itr() {}//是否还有下一个元素   public boolean hasNext() {return cursor != size;}//获取下一个元素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;//通过下标很长访问//大家思考: 如果是在获取元素其他增加了元素,那么这里通过下标获取到的和一开始期望的就不一致了//这里侧面说明,ArrayList提供的Itr也非线程安全的return (E) elementData[lastRet = i];}public void remove() {if (lastRet < 0)throw new IllegalStateException();//并发修改检查   checkForComodification();try {//lastRet在不为-1的情况下,代表着当前元素ArrayList.this.remove(lastRet);cursor = lastRet;lastRet = -1;//更新修改次数expectedModCount = modCount;//产生越界说明出现并发操作情况} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}....//只有调用了迭代器提供的remove和add方法才会更新expectedModCount的值//否则可以知道,在使用迭代器遍历当前list的期间,如果直接调用list提供的add和remove方法//那么便会更新modCount的值,而迭代器这边的expectedModCount并没有被更新//所以再次通过迭代器获取元素时,就会抛出异常了final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}}
private class ListItr extends Itr implements ListIterator {//其他方法不是不重要,而是限于篇幅原因,这里挑出典型讲清楚即可...//通过迭代器向集合增加一个元素  public void add(E e) {//并发修改检查checkForComodification();try {int i = cursor;ArrayList.this.add(i, e);cursor = i + 1;lastRet = -1;expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}}

ArrayList迭代器部分不算特别难,但是通过分析其中典型源码,我们也可以明确使用迭代器时的一些坑,当然大多数情况下,我们都不会直接使用迭代器,而是间接使用它,例如使用增强for循环遍历集合的时候,查看编译过后的java代码可以知道,本质还是利用迭代器进行的遍历,如果我们在增强for循环中对List集合进行add和remove操作,便会抛出ConcurrentModificationException异常:

public class Main {public static void main(String[] args) {ArrayList list=new ArrayList<>(20);list.add(1);list.add(2);list.add(3);for (Integer ele : list) {list.add(1);}}
}

解决方法有如下两种:

  • 调用迭代器提供的add和remove方法
public static void main(String[] args) {// 创建集合对象List list = new ArrayList();// 存储元素list.add("I");list.add("love");list.add("you");ListIterator lit = list.listIterator();while (lit.hasNext()) {String s = (String) lit.next();if ("love".equals(s)) {// add 、remove 都是可以的lit.add("❤");}System.out.print(s + " ");}System.out.println();for (Object l : list){System.out.print(l + " ");}
}//运行结果
I love you
I love ❤ you 
  • 集合遍历元素,集合修改元素(普通for)
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;public class Demo2 {public static void main(String[] args) {//创建集合对象List list = new ArrayList();//存储元素list.add("I");list.add("love");list.add("you");for (int x = 0; x < list.size(); x++){String s = (String)list.get(x);if ("love".equals(s)){list.add("❤");}System.out.print(s + " ");}}
}//运行结果
I love you ❤ 

iterator.remove() 的弊端:
Iterator 只有 remove() 方法,add 方法在 ListIterator 中有
remove 之前必须先调用 next,remove 开始就对 lastRet 做了不能小于 0 的校验,而l astRet 初始化值为 -1
next 后只能调用一次 remove,因为 remove 会将 lastRet 重新初始化为 -1


相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...