年轻人不讲武德,一起聊聊List集合(一)

红牛战神
• 阅读 1015

前言

业精于勤荒于嬉,行成于思毁于随;

在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,开始思考…

今天一起来聊一聊 List集合,看到这里,笔者懂,大家莫慌,先来宝图镇楼 ~

年轻人不讲武德,一起聊聊List集合(一)

我要打十个年轻人,敢偷袭我老同志,耗子尾汁..

咳咳.. 相信大家满脑子的ArrayList已被保国爷爷经典的画面以及台词冲淡了,那么,目的已达到,那我们言归正传,对于屏幕前帅气的猿友们来说,ArrayList,LinkedList,Vector,CopyOnWriteArrayList... 张口就来,闭眼能写,但是呢,我相信大部分的猿友们并没有刨根问底真正去看过其源码,此时,笔者帅气的脸庞似有似无洋溢起一抹微笑,毕竟是查看过源码的猿,就是那么的不讲武德,吃我一记闪电五连鞭,话不多说,来吧,展示..

一、List类图

年轻人不讲武德,一起聊聊List集合(一)

二、源码剖析


1. ArrayList(此篇详解)


  • 构造函数
    // 默认值-空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // ArrayList底层为数组 transient关键字:当前数组不能被序列化
    transient Object[] elementData; 

    /**
     * 构造函数
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

从源码中可以看出,构造只为底层数组进行初始化,默认值为空数组;

  • add() - 添加元素方法
    // ArrayList元素个数
    private int size;

    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 记录对ArrayList操作次数
    protected transient int modCount = 0;

    // ArrayList最大元素个数
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 添加入口
     */
    public boolean add(E e) {
        // 对数组进行初始化以及扩容
        ensureCapacityInternal(size + 1);
        // 数组中添加元素
        elementData[size++] = e;
        return true;
    }

    // 1.对数组进行初始化以及扩容
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    // 2.计算最小容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 判断数组是否为空,即为第一次添加元素
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // max方法:a >= b ? a : b
            // eg: 10 >= 1 -> 10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    // 3.判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        // modCount(记录修改次数) -> 在当前线程使用迭代器的过程中,如有其他线程修改了modCount(会判断是否修改操作有误(线程安全问题)) -> 抛出ConcurrentModificationException异常
        //      Fail-Fast 机制,快速失败机制,modCount声明为volatile,保证线程之间修改的可见性
        modCount++;

        // 判断是否进行扩容
        if (minCapacity - elementData.length > 0)
            // 具体扩容方法
            grow(minCapacity);
    }

    // 4.具体扩容
    private void grow(int minCapacity) {
        // 获取数据长度
        int oldCapacity = elementData.length;
        // 计算扩容后长度,第一次为例:0 + (0 >> 1) = 0 + 0 = 0
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 判断如果扩容后长度-最小容量<0,扩容后的长度为最小容量,此判断作用于第一次添加元素时
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 计算扩容后长度最大值,最大值为Integer的最大值(2^31-1)
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
        }

        // 使用 Arrays.copyOf 对我们数组容量实现扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

从源码中可以看出,添加元素时且会对数组进行初始化或者扩容;

知识点:
第一次扩容也就是第一次add时:数组长度length扩容为10,集合size为1;
第二次扩容也就是第十一次add时:数组长度length扩容为15,集合size为11;
之后每次扩容遵循以下规则,oldCapacity + (oldCapacity >> 1);

结论:
每扩容后为之前数组长度的1.5倍,最大值:Integer最大值(2^31-1),最小值:10;


  • get() - 获取元素方法
    /**
     * 入口
     */
    public E get(int index) {
        // 校验是否越界
        rangeCheck(index);

        // so easy:通过下标获取元素
        return elementData(index);
    }

    // 校验下标是否越界
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
    }

从源码中可以看出,获取元素时就是获取数组元素,通过下标直接获取即可;

  • remove() - 删除元素方法
    /**
     * 入口
     */
    public E remove(int index) {
        // 校验下标是否越界
        rangeCheck(index);

        // add()方法中已详情描述
        modCount++;

        // 获取要删除的元素
        E oldValue = elementData(index);

        /**
         * public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
         *  说明此方法参数作用:
         *      src:源数组
         *      srcPos:源数组复制的起始位置
         *      dest:目的数组
         *      destPos:目标数组放置的起始位置
         *      length:数组元素被复制的数量
         */
        // 对应参数中length
        int numMoved = size - index - 1;
        // 删除元素其实就是一个数组整体移动的过程,再将最后一个元素置空即可
        if (numMoved > 0)
            // 此每个参数都需各位猿友细品下,慢慢来,只是一个过程... 如此如此,这般这般,暖男的我在下方提供图,便于猿友们理解
            System.arraycopy(elementData, index+1, elementData, index, numMoved);

        // 将最后一个元素置空,如只有一个元素,置空即可,便于GC工作
        elementData[--size] = null; // clear to let GC do its work

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

从源码中可以看出,删除元素实则为数组移动覆盖的过程,已下图为例,便于大家理解:

  • 源数组:

年轻人不讲武德,一起聊聊List集合(一)

  • 目标数组(删除元素后的数组):

年轻人不讲武德,一起聊聊List集合(一)

  • 删除下标为0的元素(不)

结合 arraycopy(Object src, int srcPos, Object dest, int destPos, int length)来讲,可得知:

  1. src:为上述源数组;
  2. srcPos:源数组要复制的起始位置为(index+1 = 0+1 = 1)
  3. dest:为上述目标数组
  4. destPos:目标数组放置的起始位置为(index=0);
  5. length:复制的长度为(size-index-1 = 4-0-1 = 3)
  • 过程演示:

年轻人不讲武德,一起聊聊List集合(一)

  • ==划重点:==

相信之前没仔细研究过的猿友们,对ArrayList删除元素大概过程已有一些了解;

但对于有经验的开发猿来说,笔者大概能猜到两种,一种是一心追随本心道心坚固的猿友,另一种呢就是追求大道审视局势的猿友;

前者:看到这里,不论是从笔者的描述还是图文结合的理解,貌似有一定的道理,但当时的我看并不是如此,既然是arraycopy,那就不应该是移动覆盖,而是重新复制一个新数组。

后者:我当时查阅源码好像觉得也并不是这样的,记得也是复制一个新数组,而不是移动覆盖。但笔者描述确又很在理,难道...遗漏了什么?

邪魅一笑,嘴角微起,来吧,展示...
年轻人不讲武德,一起聊聊List集合(一)
其实嘛,大家说的都没错,实际上确实是复制新的数组,但ArrayList这里,源数组和目标数组是用一个呢.

哎...人生么,如此这般,细节决定成败。

  • ArrayList总结:
  1. 底层为数组;
  2. 构造初始化,数组为空数组,集合size为0,数组length为0;
    第一次扩容也就是第一次add时:数组长度length扩容为10,集合size为1;

第二次扩容也就是第11次add时:数组长度length扩容为15,集合size为11;
之后每次扩容遵循以下规则,oldCapacity + (oldCapacity >> 1),故每次扩容为之前数组长度的1.5倍;
最大值:Integer最大值2147483647(2^31-1),最小值:10;

  1. 通过下标去获取元素,故查询效率高,增删效率低;
  2. 线程不安全;
  3. 有modCount;


2. LinkedList

不讲武德,一起聊聊List集合之LinkedList

3. Vector

不讲武德,一起聊聊List集合之Vector

4. CopyOnWriteArrayList

不讲武德,一起聊聊List集合之CopyOnWriteArrayList

5. List集合对比分析-总结篇

不讲武德,一起聊聊List集合之总结篇

~~   码上福利


大家好,我是猿医生:

在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,开始学习吧...

点赞
收藏
评论区
推荐文章
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Stella981 Stella981
4年前
List的Select 和Select().tolist()
List<PersondelpnewList<Person{newPerson{Id1,Name"小明1",Age11,Sign0},newPerson{Id2,Name"小明2",Age12,
Stella981 Stella981
4年前
AssemblyScript 入门指南[每日前端夜话0xEB]
每日前端夜话0xEB每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2459 字预计阅读时间:10分钟作者:DannyGuo翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/b880277c594152a503
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
4年前
Node.js 12中的ES模块[每日前端夜话0x9E]
每日前端夜话0x9E每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2552字预计阅读时间:10 分钟作者:BrianDeSousa翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/2ccaf94cecd3
Stella981 Stella981
4年前
Github标星5300+,专门为程序员开发文档开源管理系统,我粉了
!(https://oscimg.oschina.net/oscnet/a11909a041dac65b1a36b2ae8b9bcc5c432.jpg)码农那点事儿关注我们,一起学习进步!(https://oscimg.oschina.net/oscnet/f4cce1b7389cb00baaab228e455da78d0
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
4年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
4年前
mysql查询每个学生的各科成绩,以及总分和平均分
今天看一个mysql教程,看到一个例子,感觉里面的解决方案不是很合理。问题如下:有学生表:!在这里插入图片描述(https://oscimg.oschina.net/oscnet/07b001b0c6cb7e0038a9299e768fc00a0d3.png)成绩表:!在这里插入图片描述(https://oscimg.o