今天要分享的Java集合是List,主要是针对它的常见实现类ArrayList进行讲解

内容目录

什么是List核心方法源码剖析1.文档注释2.构造方法3.add()3.remove()如何提升ArrayList的性能ArrayList可以代替数组吗?

什么是List

  List集合是线性数据结构的主要实现,用来存放一组数据。我们称之为:列表。


  ArrayList是List的一个常见实现类,它的面试频率和使用频率都非常高,所以我们今天通过学习ArrayList来对Java中的List集合有一个深入的理解。
  ArrayList最大的优势是可以将数组的操作细节封装起来,比如数组在插入和删除时要搬移其他数据。另外,它的另一大优势,就是支持动态扩容,这也是我们使用ArrayList的主要场景之一,在某些情况下我们没有办法在程序编译之前就确定存储数据
容器的大小。

核心方法源码剖析

  这一部分,选取了ArrayList的一些核心方法进行讲解。分别是:构造方法,add()、和remove()。这里有一个小窍门,我们在读jdk源码的时候,一定要先看类上的doc注释,比较核心的知识点都会写在上面。有一个初步的概念再去看源码,就会容易很多。

1.文档注释

  This class is roughly equivalent to Vector, except that it is unsynchronized.
  大致相当于Vector,不同之处是不同步(线程不安全)

  Implements all optional list operations, and permits all elements, including null
  实现所有可选列表操作,并允许所有元素,包括null

  in the face of concurrent modification, the iterator fails quickly and cleanly
  面对并发修改,迭代器将快速而干净地失败

2.构造方法

  ArrayList()提供了三种构造方法。
  ArrayList():构造一个初始容量为10的空列表。
  ArrayList(int initialCapacity):构造具有指定初始容量的空列表。
  ArrayList(Collection c):构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。

1/**
2 * Constructs an empty list with an initial capacity of ten.
3 */
4public ArrayList() {
5   this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
6}

  这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的部数组,不设定初始值时,只是引用这个内部数组。

 1/**
2 * Constructs an empty list with the specified initial capacity.
3 *
4 * @param  initialCapacity  the initial capacity of the list
5 * @throws IllegalArgumentException if the specified initial capacity
6 *         is negative
7 */
8public ArrayList(int initialCapacity) {
9    if (initialCapacity > 0) {
10        this.elementData = new Object[initialCapacity];
11    } else if (initialCapacity == 0) {
12        this.elementData = EMPTY_ELEMENTDATA;
13    } else {
14        throw new IllegalArgumentException("Illegal Capacity: "+
15                                               initialCapacity);
16    }
17}

  这里的EMPTY_ELEMENTDATA同样是一个空内部数组,为了和DEFAULTCAPACITY_EMPTY_ELEMENTDATA做区分,所以没有使用一个对象。

3.add()

  add方法是ArrayList中的一个核心方法,涉及到内部数组的扩容。

 1 /**
2  * Appends the specified element to the end of this list.
3  *
4  * @param e element to be appended to this list
5  * @return <tt>true</tt> (as specified by {@link Collection#add})
6  */
7public boolean add(E e) {
8  ensureCapacityInternal(size + 1);  // Increments modCount!!
9  elementData[size++] = e;
10  return true;
11}

  该方法是在集合中追加元素。其中核心方法是ensureCapacityInternal,意思是确定集合内部容量。

 1private void ensureCapacityInternal(int minCapacity) {
2      ensureExplicitCapacity(
3  calculateCapacity(elementData,minCapacity));
4}
5
6private static int calculateCapacity(Object[] elementData, int 
7      minCapacity) {
8  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
9    return Math.max(DEFAULT_CAPACITY, minCapacity);
10  }
11  return minCapacity;
12}

  这里首先计算了集合的容量,如果这个ArrayList是通过无参构造创建的,那么比较默认值10,以及传入的minCapacity,取最大值,这里可能有的同学会有疑问,为什么要比较默认值和minCapacity,默认值不是一定大于minCapacity吗?,这里是因为ensureCapacityInternal这个方法不仅仅是add()会调用,allAll()也会调用。

1public boolean addAll(int index, Collection<? extends E> c) {
2        rangeCheckForAdd(index);
3
4        Object[] a = c.toArray();
5        int numNew = a.length;
6        ensureCapacityInternal(size + numNew);  // Increments modCount
7       //省略部分代码..
8    }

  这里如果numNew大于10,那么默认值就会不够用。所以才会在calculateCapacity方法中引入一个求最大值的步骤。
  算出集合存储数据所需的最小空间后,就要考虑,集合原有存储空间是否够用,是否需要扩容。

 1private void ensureExplicitCapacity(int minCapacity) {
2    modCount++;
3    // overflow-conscious code
4    if (minCapacity - elementData.length > 0)
5    grow(minCapacity);
6}
7
8/**
9 * Increases the capacity to ensure that it can hold at least the
10 * number of elements specified by the minimum capacity argument.
11 *
12 * @param minCapacity the desired minimum capacity
13 */
14 private void grow(int minCapacity) {
15    // overflow-conscious code
16    int oldCapacity = elementData.length;
17    int newCapacity = oldCapacity + (oldCapacity >> 1);
18    if (newCapacity - minCapacity < 0)
19    newCapacity = minCapacity;
20    if (newCapacity - MAX_ARRAY_SIZE > 0)
21    newCapacity = hugeCapacity(minCapacity);
22    // minCapacity is usually close to size, so this is a win:
23    elementData = Arrays.copyOf(elementData, newCapacity);
24}

这里我们主要关注4个点:
  1.int newCapacity = oldCapacity + (oldCapacity >> 1);每次扩容是原数组的1.5倍
  2.扩容也是有限的,存在最大值:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
  3.集合扩容底层调用的是:Arrays.copyOf()方法,需要把数组中的数据复制一份,到新数组中,而这个方法底层是System.arrayCopy是一个native方法,效率不高。
  4.最重要的一个点:如果我们可以事先估计出数据量,那么最好给ArrayList一个初始值,这样可以减少其扩容次数,从而省掉很多次内存申请和数据搬移操作。(不指定初始值,至少会执行一次grow方法,用于初始化内部数组)。

3.remove()

 1/**
2 * Removes the element at the specified position in this list.
3 * Shifts any subsequent elements to the left (subtracts one from their
4 * indices).
5 *
6 * @param index the index of the element to be removed
7 * @return the element that was removed from the list
8 * @throws IndexOutOfBoundsException {@inheritDoc}
9 */
10public E remove(int index) {
11    rangeCheck(index);
12    modCount++;
13    E oldValue = elementData(index);
14
15    int numMoved = size - index - 1;
16    if (numMoved > 0)
17       System.arraycopy(elementData, index+1, elementData, index,
18                             numMoved);
19    elementData[--size] = null; // clear to let GC do its work
20    return oldValue;
21}

  删除的代码因为不涉及到缩容,所以比起add较为简单,首先会检查数组是否下标越界,然后会获取指定位置的元素,接着进行数据的搬移,将--size位置的元素置成null,让GC进行回收。最后将目标元素返回即可。
  另外最后我想提出一个比较容易犯的错误,集合在遍历的时候,对其结构进行修改(删除、新增元素)。举一个例子:

 1public class Test {
2    public static void main(String[] args) {
3        List<Integer> list = new ArrayList<>();
4        list.add(1);
5        list.add(2);
6        list.add(3);
7        for (Integer i : list) {
8            if(i.equals(1)){
9                list.remove(i);
10            }
11        }
12    }
13}

结果:

1Exception in thread "main" java.util.ConcurrentModificationException
2    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
3    at java.util.ArrayList$Itr.next(ArrayList.java:859)
4    at jialin.li.Test.main(Test.java:12)

  产生问题的原因,其实文档注释已经给出了明确的结果,即:
  if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own {@link ListIterator#remove() remove} or {@link ListIterator#add(Object) add} methods, the iterator will throw a {@link ConcurrentModificationException}
  如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身remove种或add方法,迭代器都将抛出一个ConcurrentModificationException。这里我建议是遍历的时候,不要对其结构进行修改,而是采用其他方法(打标,或者复制列表)的方式进行。

如何提升ArrayList的性能

  1 给定初值,省掉很多次内存申请和数据搬移操作。
  2 对于读多写少的场景,可以使用ArrayList替代LinkedList,可以省内存,同时CPU缓存的利用率也会更高。(数组存储的时候,是内存是连续的,CPU读取内存数据、内存读取磁盘数据的时候,都不是一条一条读取,而是一次读取临近的一批数据,所以连续的存储可以让CPU更有机会一次读取较多的有效数据)

ArrayList可以代替数组吗?

  不可以,任何数据结构都有它存在的场景和意义,集合没有办法存储基本数据类型,只能存储包装类型,包装类型就意味着需要拆箱和装箱,会有一定的性能消耗,如果对性能要求非常高的系统,或者只需要使用基本类型,那么就应该去使用数组而不是集合。同时数组在表示多维数据的时候,也更加直观,比如二维 int[][] 、ArrayList<arraylist>。我们使用集合更多的情况是想利用它的扩容特性,以及增删数据时不会造成空洞。

  最后,期待您的订阅和点赞,专栏每周都会更新,希望可以和您一起进步,同时也期待您的批评与指正!

一篇文章带您读懂List集合(源码分析)的更多相关文章

  1. 一篇文章带您读懂Map集合(源码分析)

    今天要分享的Java集合是Map,主要是针对它的常见实现类HashMap进行讲解(jdk1.8) 什么是Map核心方法源码剖析1.文档注释2.成员变量3.构造方法4.put()5.get() 什么是M ...

  2. 一篇文章让你读懂Pivotal的GemFire家族产品

    一篇文章让你读懂Pivotal的GemFire家族产品 学习了:https://www.sohu.com/a/217157517_747818

  3. Java 集合源码分析(一)HashMap

    目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...

  4. java集合源码分析(三):ArrayList

    概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...

  5. java集合源码分析(六):HashMap

    概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...

  6. 从Generator入手读懂co模块源码

    这篇文章是讲JS异步原理和实现方式的第四篇文章,前面三篇是: setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop 从发布订阅模式入手读懂Node.js的E ...

  7. Java集合源码分析(二)ArrayList

    ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...

  8. Java集合源码分析(一)ArrayList

    前言 在前面的学习集合中只是介绍了集合的相关用法,我们想要更深入的去了解集合那就要通过我们去分析它的源码来了解它.希望对集合有一个更进一步的理解! 既然是看源码那我们要怎么看一个类的源码呢?这里我推荐 ...

  9. Java集合源码分析(三)Vevtor和Stack

    前言 前面写了一篇关于的是LinkedList的除了它的数据结构稍微有一点复杂之外,其他的都很好理解的.这一篇讲的可能大家在开发中很少去用到.但是有的时候也可能是会用到的! 注意在学习这一篇之前,需要 ...

随机推荐

  1. ubuntu Elasticsearch环境搭建

    https://www.cnblogs.com/pigzhu/p/4705870.html

  2. Caffe Ubuntu14.04 + CUDA 8 (支持GTX1080 1070等Pascal架构显卡)

    1. 前言 本教程使用的系统是Ubuntu 14.04 LTS 64-bit,使用的CUDA版本为8. 理论上本教程支持Pascal架构显卡,如游戏卡GeForce GTX1070,GTX 1080, ...

  3. Uncaught (in promise) NavigationDuplicated {_name: "NavigationDuplicated"}的解决方法

    左侧菜单栏时,发现点击路由跳转相同地址 会有这个报错 Uncaught (in promise) NavigationDuplicated {_name: "NavigationDuplic ...

  4. 在VMware装了linux系统,如何在windows系统中用xshell连接

    网上有好几种方法,不过我觉得这种比较简单 1.找到VMware菜单  打开 编辑>虚拟网络编辑器 如图: 点下面的更改设置 点确定就可以了,什么都不用改.然后回到linux系统中ifconfig ...

  5. Docker系列三:Dockerfile

    Dockerfile是由一系列命令和参数构成的脚本,这些命令应用于基础镜像并最终创建一个新的镜像 Dockerfile由一行行命令语句组成,支持#开头的注释 Dockerfile分为四部分:基础镜像信 ...

  6. 九成AI企业亏损,人工智能商业落地为何这么难?

    自1956年"人工智能"一词诞生于"达特茅斯会议"后,前者就始终在不断向前推进.虽然中间经历了不少低谷和寒潮,但总算挺了过来.60多年后,人工智能在当下呈现突飞 ...

  7. vue2.0学习之路由

    下载vue-router: cnpm install vue-router --save router/main.js /*引入所需要的组件*/ import VueRouter from 'vue- ...

  8. python语法基础-函数-内置函数和匿名函数-长期维护

    ##################     内置函数        #######################  """ 一共是 68个内置函数: 反射相关的内置函 ...

  9. 3dmax2019卸载/安装失败/如何彻底卸载清除干净3dmax2019注册表和文件的方法

    3dmax2019提示安装未完成,某些产品无法安装该怎样解决呢?一些朋友在win7或者win10系统下安装3dmax2019失败提示3dmax2019安装未完成,某些产品无法安装,也有时候想重新安装3 ...

  10. java中的二维数组基础知识

    二维数组基本知识,毕竟常见的有:概念,初始化,遍历 概念: 理解二维数组,首先要先理解一维数组是什么.一维数组是个容器,存储相同数据类型的容器(这里不再做一位数组的具体介绍).二维数组就是用来存储一维 ...