前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JAVA集合之ArrayList源码分析

JAVA集合之ArrayList源码分析

作者头像
黎明大大
发布2021-03-25 14:43:18
2450
发布2021-03-25 14:43:18
举报
文章被收录于专栏:java相关资料java相关资料

点击“蓝字”关注我们吧

前言

该文章是基于JDK1.8版本进行集合源码分析的,工具使用的是IDEA哈。

简介

ArrayList的概述

ArrayList是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储任意类型的数据,ArrayList是线程不安全的,非常适合用于对元素进行查找,效率非常高,且它的时间复杂度为o(1)

ArrayList的数据结构

分析一个容器的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思路,具体的实现细节再具体分析。

那么ArrayList的数据结构如下图:

看到上图,我们就知道,它的底层数据结构就是一个数组嘛,它的元素数据类型为Object类型,意味着可以存储所有类型的数据,所以我们对ArrayList的所有实例操作底层都是基于数组进行实现的。

ArrayList集合源码分析

首先看看ArrayList的结构图

分析结构

为什么要先继承AbstractList,而让AbstractList先实现List<E>?而不是让ArrayList直接实现List<E>?

这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。

ArrayList实现了哪些接口?

  • List<E>接口:

我们会出现这样一个疑问,在查看了ArrayList的父类AbstractList也实现了List<E>接口,那为什么子类ArrayList还是去实现一遍List<E>接口呢?这是想不通的地方,所以我就去查资料,有的人说是为了查看代码方便,使观看者一目了然,说法不一,但每一个让我感觉合理的,但是在stackOverFlow中找到了答案,这里其实很有趣。网址贴出来 http://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete开发这个collection 的作者Josh说。这其实是一个mistake,因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。

  • RandomAccess接口:

这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如arrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。

  • Cloneable接口:

实现了该接口,就可以使用Object.Clone()方法了。

  • Serializable接口:

实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后使用反序列化还能将字节流变成原来的类。

类中属性

代码语言:javascript
复制
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 版本号
    private static final long serialVersionUID = 8683452581122892189L;
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 默认空对象数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 元素数组
    transient Object[] elementData;
    // 实际元素大小,默认为0
    private int size;
    // 最大数组容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

构造方法分析

ArrayList有三个构造方法:

无参构造方法

代码语言:javascript
复制
/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    //DEFAULTCAPACITY_EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData也是个Object[]类型。this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

有参构造方法一

代码语言:javascript
复制
public ArrayList(Collection<? extends E> c) {
    //将集合转为数组
    elementData = c.toArray();
    //将elementData.length 赋值给 size,如果size的长度为0,则进行初始化为一个空数组
    if ((size = elementData.length) != 0) {
        //每个集合的toArray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么就需要使用ArrayList中的方法去改造一下。if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 替换为空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

有参构造方法二

代码语言:javascript
复制
/**
 * @param initialCapacity 用户自定义初始化容量
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        //如果用户定义的初始化容量大于0,则直接将数组容量扩容到用户定义的容量
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //如果用户定义的初始化容量等于0,则直接构造一个空的数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //如果用户定义的初始化容量小于0,则直接抛出异常
        throw new IllegalArgumentException("Illegal Capacity: " +
                initialCapacity);
    }
}

核心方法分析

add方法

add(E e)源码分析

代码语言:javascript
复制
/**
 * 最为常用的添加方法
 *
 * @param e 添加元素
 * @return
 */
@Override
public boolean add(E e) {
    //确定内部容量是否够了,size是全局变量,该变量就是数组中数据的个数,因为要添加一个元素,所以size+1,
    ensureCapacityInternal(size + 1);
    //在数组中正确的位置上放上元素e,并且size++ (ps:个人认为size++和size+1可以做成一个变量然后通用,而不是分两步进行操作,其实在这个地方也可以看的出线程不安全)
    elementData[size++] = e;
    return true;
}

进入到ensureCapacityInternal方法,该方法的作用是,确认容量是否够用

代码语言:javascript
复制
/**
 * 确定内部容量的方法
 *
 * @param minCapacity 最小容量数
 */
private void ensureCapacityInternal(int minCapacity) {
    //ensureExplicitCapacity:判断是否需要进行扩容
    //calculateCapacity:确认首次扩容大小
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

上面嵌套方法了,我们先进入calculateCapacity方法,该方法的作用在于首次确认扩容大小

代码语言:javascript
复制
/**
 * 确认首次扩容大小
 *
 * @param elementData 元素数组
 * @param minCapacity 需要最小容量
 * @return
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //初次扩容判断元素数组是否为空,如果为空则首次扩容长度为10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //Math.max API作用:比较两个值,返回最大的值
        //DEFAULT_CAPACITY:默认值为10
        //minCapacity:首次进来这里就是1啦
        //最后返回10出去,不过这里还没有实现真正扩容哦
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

分析完calculateCapacity方法后,我们知道该方法主要作用是确认首次扩容大小,接着我们在返回到上一层方法ensureExplicitCapacity方法,这个方法内部有个逻辑判断是否要进行扩容,如果需要进行扩容,则执行grow方法,实现真正的容器扩容

代码语言:javascript
复制
/**
 * 判断是否需要进行扩容
 *
 * @param minCapacity 需要最小扩容数
 */
private void ensureExplicitCapacity(int minCapacity) {
    //该处的作用:就是一个安全机制,记录修改次数的,相当于版本号,关于Fail-Fast机制,后面详谈
    modCount++;

    //minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用,不够用那么就要增加elementData的length。   if (minCapacity - elementData.length > 0) {
        //那就交给grow方法扩张容量去吧。       grow(minCapacity);
    }
}

这里有的伙伴就会模糊minCapacity到底是什么呢,这里给你们分析一下

第一种情况:由于elementData初始化时是空的数组,那么第一次add的时候,minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量ensureCapacityInternal)就会判断出是空的数组,就会给将minCapacity=10,到这一步为止,还没有改变elementData的大小。

第二种情况:elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length是否够用,如果length不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。

好了,到了grow方法,这里就是add方法实际扩容的地方啦

代码语言:javascript
复制
/**
 * 扩容核心方法
 *
 * @param minCapacity
 */
private void grow(int minCapacity) {
    //原来的元素数组容量
    int oldCapacity = elementData.length;
    //newCapacity容量是oldCapacity的1.5倍容量  或者说 newCapacity的容量是基于oldCapacity扩容50%容量 (在初始化的时候这里是没有任何元素的哦)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //前面的初始化容量10就是为这个判断进行准备的,所以这里就是真正初始化elementData的大小了
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    //如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        newCapacity = hugeCapacity(minCapacity);
    }
    
    //实际进行数组扩容啦
    elementData = Arrays.copyOf(elementData, newCapacity);
}

可能有的伙伴对Arrays.copyOf这个API有点陌生,这里简单给小伙伴解释一下。

Arrays.copyOf的意思:方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组

简单的意思是:原来elementData总大小为0,但是newCapacity为10的话,那么第一个参数小于第二个参数则会创建一个新的数组对象,新的数组对象长度为第二个参数的值,然后会将原来的值copy到新的数组中

这里有篇博客通俗易懂的代码案例:https://blog.csdn.net/qq_25131363/article/details/85001414

最后来到hugeCapacity方法,一般情况下几乎进不到该方法,因为如果扩容的长度大于MAX_ARRAY_SIZE(Integer的最大值 -8 就是2的32次方 -1 -8 ),那么扩容的长度就为Integer的最大值。之所以我前面说为啥会进不到该方法,我们平常也不会有这么多数据到集合中。

代码语言:javascript
复制
/**
 * 这个就是上面用到的方法,很简单,就是用来赋最大值
 *
 * @param minCapacity
 * @return
 */
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) {
        throw new OutOfMemoryError();
    }
    //如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回。   //Integer.MAX_VALUE:2147483647   MAX_ARRAY_SIZE:2147483639  也就是说最大也就能给到第一个数值。还是超过了这个限制,就要溢出了。相当于arraylist给了两层防护。   return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

add(int index, E element)源码分析

代码语言:javascript
复制
/**
 * 添加元素到指定下标位置中
 *
 * @param index
 * @param element
 */
public void add(int index, E element) {

    //验证index的合法性
    rangeCheckForAdd(index);
    
    //跟上面分析的一样,具体看上面如何分析的
    ensureCapacityInternal(size + 1);
    //这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位
    System.arraycopy(elementData, index, elementData, index + 1,
            size - index);
    //在目标位置上存放元素
    elementData[index] = element;
    //size增加1
    size++;
}

分析下标越界rangeCheckForAdd方法

代码语言:javascript
复制
/**
 * 验证Index下标是否越界
 * @param index
 */
private void rangeCheckForAdd(int index) {
    //如果index超过实际数组大小 || 下标小于0 直接返回下标越界异常
    if (index > size || index < 0) {
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
}

分析提示异常outOfBoundsMsg方法

代码语言:javascript
复制
/**
 * 下标越界提示
 *
 * @param index
 * @return
 */
private String outOfBoundsMsg(int index) {
    return "Index: " + index + ", Size: " + size;
}

add(Collection<? extends E> c)源码分析

代码语言:javascript
复制
public boolean addAll(Collection<? extends E> c) {

    //将集合数据 转为 数组数据
    Object[] a = c.toArray();

    //numNew为数组的长度
    int numNew = a.length;

    //确认是否扩容,这个方法可以先看add是如何分析的
    ensureCapacityInternal(size + numNew);

    /**
     *
     * 主要用来移动元素的,这也就是为什么arrayList使用增删的时候效率低的原因
     *
     * 五个参数意思:
     * Object src:源对象
     * int  srcPos:源对象对象的起始位置
     * Object dest:目标对象
     * int destPos:目标对象的起始位置
     * int length:从起始位置往后复制的长度。    */
    System.arraycopy(a, 0, elementData, size, numNew);

    //更新元素数据长度
    size += numNew;
    return numNew != 0;
}

remove方法

remove(int index)源码分析

代码语言:javascript
复制
public E remove(int index) {
    //检查下标合理性,rangeCheck方法前面分析过了,这里就不分析了。   rangeCheck(index);

    //这个作用很多,比如用来检测快速失败的一种标志。   modCount++;

    //根据下标获取数据
    E oldValue = elementData(index);

    //计算移动的位数
    int numMoved = size - index - 1;

    if (numMoved > 0) {
        //主要用来移动元素的
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    }

    //将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。   elementData[--size] = null;

    //返回删除的元素。   return oldValue;
}

remove(Object o)源码分析

代码语言:javascript
复制
//感觉这个不怎么要分析吧,都看得懂,就是通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemobe(index),使用这个方法来删除该元素,
//fastRemove(index)方法的内部跟remove(index)的实现几乎一样,这里最主要是知道arrayList可以存储null值
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;
}

removeAll(Collection<?> c)源码分析

代码语言:javascript
复制
public boolean removeAll(Collection<?> c) {
    //判断传入的元素是否为null
    Objects.requireNonNull(c);
    //批量删除
    return batchRemove(c, false);
}

batchRemove(Collection<?> c, boolean complement)源码分析

代码语言:javascript
复制
//该方法用于其他两个方法中,一个removeAll():它只清楚指定集合中的元素,retainAll()用来测试两个集合是否有交集。 
private boolean batchRemove(Collection<?> c, boolean complement) {
    //将原数组数据 赋值给 elementData 变量
    final Object[] elementData = this.elementData;
    //定义两个临时变量  r用来控制循环,w是记录有多少个交集
    int r = 0, w = 0;
    boolean modified = false;
    try {
        //循环size长度的次数
        for (; r < size; r++)
            //参数中的集合C一次检测集合A中的元素是否有
            //判断参数中的集合C是否包含
            //判断elementData[r]下标的数据在参数C集合中是否存在,如果存在就将elementData[r]下标的值存放到elementData[w++]数组下标位置中
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        //如果contains方法使用过程报异常
        if (r != size) {
            //将剩下的元素都赋值给集合A
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            //这里有两个用途,
            //在removeAll()时,w一直为0,就直接跟clear一样,全是为null。           //retainAll():没有一个交集返回true,有交集但不全交也返回true,而两个集合相等的时候,返回false,
            //所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。           for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

clear()方法

清除元素数组中所有值

代码语言:javascript
复制
//清除集合中所有的数据
public void clear() {
    //这里就是一种安全机制,下面会分析它
    modCount++;

    //这里就是遍历集合的所有数据,将他们的下标值全部置为NULL,等待GC回收掉内存
    for (int i = 0; i < size; i++)
        elementData[i] = null;
    size = 0;
}

set方法

下面的方法主要用处在于设定指定下标索引的元素值

代码语言:javascript
复制
//该方法的作用在于,将指定下标的值进行替换
public E set(int index, E element) {
    //上方有分析到,该方法就是检查下标的合法性的
    rangeCheck(index);
    
    //从元素数组中获取数据
    E oldValue = elementData(index);
    //将数据替换到指定下标中
    elementData[index] = element;
    return oldValue;
}

indexOf方法

其实从下面方法中,我们可以了解到ArrayList集合是允许存放NULL值的,且该方法查找值也是从头开始遍历到尾。

代码语言:javascript
复制
//该方法的作用在于,从元素数组中查找数据的下标位置(其实就是查找数据是否存在啦)
public int indexOf(Object o) {
    //查找元素为Null
    if (o == null) {
        // 遍历数组,找到第一个为空的元素,返回下标
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        //查找元素不是为NUll
        for (int i = 0; i < size; i++)
            // 遍历数组,找到第一个和指定元素相等的元素,返回下标
            if (o.equals(elementData[i]))
                return i;
    }
    //没有找到直接返回-1啦
    return -1;
}

get方法

检索数据,获取指定下标的数据

代码语言:javascript
复制
public E get(int index) {
    //检查index的合法性
    rangeCheck(index);
    //从元素数组下标中获取数据
    return elementData(index);
}

看到这里,相信这里我不用解释,小伙伴就知道什么意思了吧

代码语言:javascript
复制
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

最后来聊聊fail-fast快速失败机制和modcount

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

FastFailTest中通过 new ThreadOne().start() 和 new ThreadTwo().start() 同时启动两个线程去操作list。

ThreadOne线程:向list中依次添加0,1,2,3,4,5。每添加一个数之后,就遍历整个list。

ThreadTwo线程:向list中依次添加10,11,12,13,14,15。每添加一个数之后,就遍历整个list。

当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出ConcurrentModificationException异常,产生fail-fast事件。

modCount:其实就是一种安全机制 每add 或 remove的时候就会进行加1,其实简单理解,可以认为这个就是乐观锁(版本号机制啦),当多个线程操作集合数据的时候, 一个线程在查询数据,一个线程在修改数据,就导致数据不一致的发生,当出现这种情况就会触发 fail-fast机制

总结

1.ArrayList集合是允许存放NULL值的 2.ArrayList集合本质上就是一个元素数组 3.ArrayList集合与数组的区别在于ArrayList可以自动进行扩容 4.ArrayList集合实现了RandomAccess接口,也就意味着使用for循环效率会更高 5.ArrayList集合底层是采用的数组进行存储数据,那么它的查询速度会很快,但是添加或删除数据,性能则会下降很多。

本文参考:https://www.cnblogs.com/zhangyinhua/p/7687377.html

我是黎明大大,我知道我没有惊世的才华,也没有超于凡人的能力,但毕竟我还有一个不屈服,敢于选择向命运冲锋的灵魂,和一个就是伤痕累累也要义无反顾走下去的心。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黎明大大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档