今天要分享的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. Graylog

    Graylog #Graylog 是与 ELK 可以相提并论的一款集中式日志管理方案,支持数据收集.检索.可视化 ​#Graylog 架构 - Graylog 负责接收来自各种设备和应用的日志,并为用 ...

  2. 在线好用的json转xml超级好用在线json与xml互相转换

    在线好用的json转xml超级好用在线json与xml互相转换 拿走不谢:http://www.yzcopen.com/json/jsonxmlformat

  3. JacksonUtil

    package org.linlinjava.litemall.core.util; import com.fasterxml.jackson.core.type.TypeReference; imp ...

  4. k8s中command、args和dockerfile中的entrypoint、cmd之间的关系

    当用户同时写了command和args的时候自然是可以覆盖DockerFile中ENTRYPOINT的命令行和参数,那么对于具体情况呢,比如仅仅写了command或者args的时候呢?完整的情况分类如 ...

  5. git的命令操作指南

    Git图形化界面我用的还可以,但是命令就不太会了,索性和大家一起学习下Git命令的用法...一般来说,日常使用只要记住下图6个命令,就可以了.但是熟练使用,恐怕要记住60-100个命令. fetch ...

  6. java 变量分类

    转:https://blog.csdn.net/suneqing/article/details/37909811 1.按被声明的位置划分 局部变量:方法和语句块内定义的的变量.(在定义局部变量时,必 ...

  7. 收集到的技术相关网址——python

    1.Python中常用数据库访问接口模块 专用数据库连接模块——MySQL.SQLite.PostgreSQL.Oracle.IBM DB2.Infomix.Interbase.Sybase.SQL ...

  8. ZOJ-1167-Trees on the Level

    题解: 我的解法是用一个类似字典树结构的结构体来表示节点.看到另一种解法是用数组来映射二叉树的,开到14000就过了,但是我觉得是数据水了,因为题中说最多 256个节点,如果256个节点连成链型,除根 ...

  9. 18)添加引号转移函数,防止SQL注入

    目录机构: 然后我的改动代码: MysqlDB.class.php <?php /** * Created by PhpStorm. * User: Interact * Date: 2017/ ...

  10. http、https的压测工具——apacheab 、webbench

    http的压测工具 搞清楚不同的安装方法,执行文件.配置文件的路径. yum安装: 执行文件在/usr/bin 下,一般为执行文件 配置文件在/etc目录下 conf文件 源码安装: 执行文件在安装文 ...