大学专科实习第二个月——ArrayList底层实现原理

简介:以上文章讲述的是HTTPS底层原理的实现接下来讲解的是ArrayList底层实现原理。觉得我还可以的可以加群探讨技术QQ群:1076570504 个人学习资料库http://www.aolanghs.com/ 微信公众号搜索【欢少的成长之路】

前言

这是我实习的第二个月。今天讲述的是ArrayList。大家肯定对这个很熟悉,比如日常开发过程中,不管是前后端分离开发还是不分离开发,都会利用接口调用SQL语句查询数据。查询到的结果是存入ArrayList里的。问题来了!

  1. ArrayList是什么?
  2. ArrayList优点与缺点是什么?
  3. 为啥ArrayList在操作的时候,线程不安全还使⽤他呢?
  4. ArrayList底层是数组,不断添加数据不会出问题吗?数组是有限制的,ArrayList不受限制,它是怎么实现的呢?
  5. JDK1.7版本与JDK1.8版本初始化的时候区别在哪里?
  6. ArrayList的缺点新增慢的问题,为什么慢?它是怎么做的?
  7. ArrayList(int initialCapacity)会不会初始化数组⼤⼩?
  8. ArrayList插⼊删除⼀定慢么?
  9. ArrayList删除是怎么实现的?
  10. 以上说到了ArrayList的缺点中有一个线程的问题,那它的线程安全吗?
  11. ArrayList⽤来做队列合适么?那数组适合⽤来做队列么?
  12. ArrayList的遍历和LinkedList遍历性能⽐较如何?
  13. 常用的ArrayList函数总结!

通过这篇文章你能学习到这些知识!想了解的继续深入,不想了解的赶紧离开,我不想浪费你们的学习时间。找准自己的定位与目标,坚持下去,并且一定要高效。我跟你们一样非常抵制垃圾文章,雷同文章,胡说八道的文章。

很多人会问,学底层多浪费时间,搞好实现功能不就好了吗?

可以这样想一下到了一定的工作年限技术的广度深度都有一定的造诣了,你写代码就这样了没办法优化了,机器配置也是最好的了,那还能优化啥?​ 底层,我们都知道所有语言到最后要运行都是变成机器语言的,最后归根究底都是要去跟机器交互的,那计算机的底层是不是最后还是要关注的东西了!

正文

1.ArrayList是什么?

ArrayList是数组列表,主要用来装载数据,当我们装在的是基本类型int,long,boolean,short,byte的时候我们只能存储他们对应的包装类,它的主要底层实现是数组Object[] elementData。

与它类似的是LinkedList。和LinkedList相比,它的查找和访问元素的速度更快,但新增,删除的速度较慢。

2.ArrayList优点与缺点是什么?

优点:查询效率高,使用频率也很高
缺点:线程不安全,增删效率低

3.为啥ArrayList在操作的时候,线程不安全还使⽤他呢?

因为我们正在使用的场景中,都是用来查询存放相应的数据,然后转成json返回给前端,不会涉及太多频繁的增删,如果涉及频繁的增删,可以使用LinedList,如果你需要线程安全就使用Vector,这就是这三者的区别了,实际开发过程中还是ArrayList使用最多的。

任何东西有他相应的价值,鱼与熊掌不可兼得!所以不存在一个集合工具是查询效率又高,增删效率又高,线程也安全的。⾄于为啥⼤家看代码就知道了,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。(Tip:这里强调一些小知识,不要为了用而去用,用之前一定要了解它的性能以及线程安全啥的。首选适合自己的!)

4.ArrayList底层是数组,不断添加数据不会出问题吗?数组是有限制的,ArrayList不受限制,它是怎么实现的呢?

ArrayList可以通过构造⽅法在初始化的时候指定底层数组的⼤⼩。
通过⽆参构造⽅法的⽅式ArrayList()初始化,则赋值底层数Object[] elementData为⼀个默认空数组Object[]DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以数组容量为0,只有真正对数据进⾏添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。
⼤家可以分别看下他的⽆参构造器和有参构造器,⽆参就是默认⼤⼩,有参会判断参数。

其实实现方法很简单,他就是通过数组扩容的方式去实现的。比如我们现在有一个长度为10的数组,现在我们要新增一个元素,发现已经满了,ArrayList他会重新定义一个长度为10+10/2的数组也就是新增一个长度为15的数组。然后把原数组的数据,原封不动的复制到新数组中,然后再把指向原数据的地址切换到扩容后的数组,这样Array就实现了一次改头换面。(我们在使用ArrayList不会设置初始值的大小,默认大小是10,如果你传入了参数,他就会优先使用你传入参数来设定。否则就默认)
在这里插入图片描述

5.JDK1.7版本与JDK1.8版本初始化的时候区别在哪里?

ArrayList1.8 开始变化有点⼤,⼀个是初始化的时候,1.7会调⽤this(10)的无参构造方法真正的容量为10

ArrayList1.8的构造方法中指定的数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {},是个空数组,数组长度为0,所以说JDK1.8下的()初始化后默认的数组长度为0.

//无参构造方法   1.7
public ArrayList() {
    
    
        this(10);
}

 public ArrayList(int initialCapacity) {
    
    
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
}

//无参构造方法   1.8
public ArrayList() {
    
    
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
    
    };

既然1.8以后ArrayList是空数组那它是怎么添加第一个元素的呢?

在ensureCapacityInternal方法里,elementData是当前数组,if条件返回true,minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity)=10
在ensureExplicitCapacity方法里,minCapacity - elementData.length等于10-0之后就进入grow函数了

public boolean add(E e) {
    
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}
private void ensureCapacityInternal(int minCapacity) {
    
    
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    
    
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
    
    
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
private void grow(int minCapacity) {
    
    
        // overflow-conscious code
        int oldCapacity = elementData.length;  //oldCapacity =0
        int newCapacity = oldCapacity + (oldCapacity >> 1);  //newCapacity =0+0=0
        if (newCapacity - minCapacity < 0)   //newCapacity - minCapacity=0-10<0
            newCapacity = minCapacity;       //newCapacity = minCapacity=10
        if (newCapacity - MAX_ARRAY_SIZE > 0)   //newCapacity - MAX_ARRAY_SIZE<0
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);  //将原数组的值复制到新数组,新数组的长度是10
    }

综上所述,JDK1.8中ArrayList()初始化后的底层数组长度为0,且在添加第一个元素时,底层数据长度变为10,之后扩容按原来的1.5倍进行扩容。

6.ArrayList的缺点新增慢的问题,为什么慢?它是怎么做的?

他有指定的index新增,也有直接新增的,在这之前他会有一步校验长度的判断ensureCapacityInternal,也就是说如果数组长度不够,是需要扩容的。在扩容的时候老版本的jdk和8以后的版本是有区别的,8之后效率更高了,采用位运算,右移一位就是除以2。
JDK1.7版本:3/2+1
JDK1.8版本:3/2
在这里插入图片描述
在指定的位置上新增的时候,校验之后的操作就比较简单了,就是数组的copy。不知道大家有没有看懂arraycopy,画个图解释一下,你可能就明白一点了:

下面的这个数组我需要在少后面的这个位置新增一个元素add,从代码里可以看到,他复制了一个数组,是从index少开始的,然后把他放在了index少+1的位置。给将要添加的元素腾出一个位置。然后就完成操作了。效率低的原因就是 如果数据量过大庞大的话就需要把后面所有的元素都复制了然后再开始进行扩容操作一系列操作走下来不就慢了嘛!
在这里插入图片描述

在这里插入图片描述

7.ArrayList(int initialCapacity)会不会初始化数组大小?

会初始化数组的大小!但是List的大小没有变,因为list的大小是返回size的。

一般情况下我们设置了初始大小,但是我们打印的size大小是0,我们操作下标的时候也会报错,数组下标越界。其实数组是初始化了,但是List没有,那size就没变,在我们使用set函数的时候是和size函数返回的值比较的所以就会出现报错。
在这里插入图片描述

8.ArrayList插⼊删除⼀定慢么?

取决于你删除的元素离数组末端有多远,ArrayList拿来作为堆栈来⽤还是挺合适的,push和pop操作完
全不涉及数据移动操作。
离末端有多远没听懂的话继续看第九问!

9.ArrayList删除是怎么实现的?

删除跟新增的原理是一样的,都是copy数组!上图!
在这里插入图片描述
开始!我们现在要删除add这个元素,代码执行流程就是复制add+1的元素~最后,然后把他放在indexadd的这个位置。所以add这个元素就被这个覆盖了,给了你删除数据的感觉!
跟新增也是一样的 如果数据末端的数据过于庞大呢?执行复制然后再移动就慢了呀。
在这里插入图片描述

10.以上说到了ArrayList的缺点中有一个线程的问题,那它的线程安全吗?

当然不是,线程安全版本的数组容器是Vector。
Vector的实现很简单,就是把所有的⽅法统统加上synchronized就完事了。
你也可以不使⽤Vector,⽤Collections.synchronizedList把⼀个普通ArrayList包装成⼀个线程安全版本
的数组容器也可以,原理同Vector是⼀样的,就是给所有的⽅法套上⼀层synchronized。

11.ArrayList⽤来做队列合适么?那数组适合⽤来做队列么?

队列⼀般是FIFO(先⼊先出)的,如果⽤ArrayList做队列,就需要在数组尾部追加数据,数组头部删除
数组,反过来也可以。
但是⽆论如何总会有⼀个操作会涉及到数组的数据搬迁,这个是⽐较耗费性能的。
结论:ArrayList不适合做队列。

数组是⾮常合适的。
⽐如ArrayBlockingQueue内部实现就是⼀个环形队列,它是⼀个定⻓队列,内部是⽤⼀个定⻓数组来实
现的。
另外著名的Disruptor开源Library也是⽤环形数组来实现的超⾼性能队列,具体原理不做解释,⽐较复
杂。
简单点说就是使⽤两个偏移量来标记数组的读位置和写位置,如果超过⻓度就折回到数组开头,前提是
它们是定⻓数组。

12.ArrayList的遍历和LinkedList遍历性能⽐较如何?

论遍历ArrayList要⽐LinkedList快得多,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存
结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销。

13.常用的ArrayList函数总结!

ArrayList就是动态数组,⽤MSDN中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,
实现了ICollection和IList接⼝,灵活的设置数组的⼤⼩等好处。
⾯试⾥⾯问的时候没HashMap,ConcurrentHashMap啥的这么常问,但是也有⼀定概率问到的,还是
那句话,不打没把握的仗。
我们在源码阅读过程中,不需要全部都读懂,需要做的就是读懂核⼼的源码,加深⾃⼰对概念的理解就
好了,⽤的时候不⾄于啥都不知道,不要为了⽤⽽⽤就好了

boolean add(E e)
将指定的元素添加到此列表的尾部。

void add(int index, E element)
将指定的元素插⼊此列表中的指定位置。

boolean addAll(Collection<? extends E> c)
按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。

boolean addAll(int index, Collection<? extends E> c)
从指定的位置开始,将指定 collection 中的所有元素插⼊到此列表中。

void clear()
移除此列表中的所有元素。

Object clone()
返回此 ArrayList 实例的浅表副本。

boolean contains(Object o)
如果此列表中包含指定的元素,则返回 truevoid ensureCapacity(int minCapacity)
如有必要,增加此 ArrayList 实例的容量,以确保它⾄少能够容纳最⼩容量参数所指定的元素数。

E get(int index)
返回此列表中指定位置上的元素。

int indexOf(Object o)
返回此列表中⾸次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1boolean isEmpty()
如果此列表中没有元素,则返回 true

int lastIndexOf(Object o)
返回此列表中最后⼀次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。

E remove(int index)
移除此列表中指定位置上的元素。

boolean remove(Object o)
移除此列表中⾸次出现的指定元素(如果存在)。

protected void removeRange(int fromIndex, int toIndex)
移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。

E set(int index, E element)
⽤指定的元素替代此列表中指定位置上的元素。

int size()
返回此列表中的元素数。

Object[] toArray()
按适当顺序(从第⼀个到最后⼀个元素)返回包含此列表中所有元素的数组。

T[] toArray(T[] a)
按适当顺序(从第⼀个到最后⼀个元素)返回包含此列表中所有元素的数组;返回数组的运⾏时类型是
指定数组的运⾏时类型。

void trimToSize()
将此 ArrayList 实例的容量调整为列表的当前⼤⼩。

公众号请求各位小哥哥小姐姐关注!每天分享底层原理文章

在这里插入图片描述

结尾

小白一枚技术不到位,如有错误请纠正!最后祝愿广大的程序员开发项目的时候少遇到一些BUG。正在学习的小伙伴想跟作者一起探讨交流的请加下面QQ群。

知道的越多,不知道的就越多。找准方向,坚持自己的定位!加油向前不断前行,终会有柳暗花明的一天!
文章将持续更新,我们下期见!【下期将更新集合类原理 QQ群:1076570504 微信公众号搜索【欢少的成长之路】请多多支持!

猜你喜欢

转载自blog.csdn.net/weixin_44907128/article/details/112650424