从今天开始也正式开 JDK 原理分析的坑了,其实写源码分析的目的不再是像以前一样搞懂原理,更重要的是看看他们编码风格更进一步体会到他们的设计思想。看源码前先自己实现一个再比对也许会有不一样的收获!

1. 结构

  首先我们需要对 ArrayList 有一个大致的了解就从结构来看看吧.

1. 继承

  该类继承自 AbstractList 这个比较好说

2. 实现

这个类实现的接口比较多,具体如下:

  1. 首先这个类是一个 List 自然有 List 接口
  2. 然后由于这个类需要进行随机访问,所谓随机访问就是用下标任一访问,所以实现了RandomAccess
  3. 然后就是两个集合框架肯定会实现的两个接口 Cloneable, Serializable 前面这个好说序列化一会我们具体再说说

3. 主要字段

  1. // 默认大小为10
  2. private static final int DEFAULT_CAPACITY = 10;
  3. // 空数组
  4. private static final Object[] EMPTY_ELEMENTDATA = {};
  5. // 默认的空数组 这个是在传入无参的是构造函数会调用的待会再 add 方法中会看到
  6. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  7. // 用来存放 ArrayList 中的元素 注意他的修饰符是一个 transient 也就是不会自动序列化
  8. transient Object[] elementData;
  9. // 大小
  10. private int size;

4. 主要方法

下面的方法后面标有数字的就是表示重载方法

  1. ctor-3
  2. get
  3. set
  4. add-2
  5. remove-2
  6. clear
  7. addAll
  8. write/readObject
  9. fast-fail 机制
  10. subList
  11. iterator
  12. forEach
  13. sort
  14. removeIf

2. 构造方法分析

1. 无参的构造方法

   里面只有一个操作就是把 elementData 设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组。

  1. // 无参的构造函数,传入一个空数组 这时候会创建一个大小为10的数组,具体操作在 add 中
  2. public ArrayList() {
  3. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  4. }

2. 传入数组大小的构造

   这个就是 new 一个数组,如果数组大小为0就 赋值为 EMPTY_ELEMENTDATA

  1. // 按传入的参数创建新的底层数组
  2. public ArrayList(int initialCapacity) {
  3. if (initialCapacity > 0) {
  4. this.elementData = new Object[initialCapacity];
  5. } else if (initialCapacity == 0) {
  6. this.elementData = EMPTY_ELEMENTDATA;
  7. } else {
  8. throw new IllegalArgumentException("Illegal Capacity: "+
  9. initialCapacity);
  10. }
  11. }

3. 传入 Collection 接口

   在这个方法里面主要就是把这个 Collection 转成一个数组,然后把这个数组 copy 一下,如果这个接口的 size 为0 和上面那个方法一样传入 EMPTY_ELEMENTDATA

  1. public ArrayList(Collection<? extends E> c) {
  2. elementData = c.toArray();
  3. if ((size = elementData.length) != 0) {
  4. // c.toArray might (incorrectly) not return Object[] (see 6260652)
  5. // 上面的注释的意思是说 jdk 有一个 bug 具体来说就是一个 Object 类型的数组不一定能够存放 Object类型的对象,有可能抛异常
  6. // 主要是因为 Object 类型的数组可能指向的是他的子类的数组,存 Object 类型的东西会报错
  7. if (elementData.getClass() != Object[].class)
  8. // 这个操作是首先new 了新的数组,然后再调用 System.arraycopy 拷贝值。也就是产生新的数组
  9. elementData = Arrays.copyOf(elementData, size, Object[].class);
  10. } else {
  11. // 传入的是空的就直接使用空数组初始化
  12. this.elementData = EMPTY_ELEMENTDATA;
  13. }
  14. }

   但是注意一点这里有一个 jdk 的 bug 也就是一个 Object 类型的数组不一定能够存放 Object类型的对象,有可能抛异常,主要是因为 Object 类型的数组可能指向的是他的子类的数组,存 Object 类型的东西会报错。 为了测试这个 bug 写了几行代码测试一下。这个测试是通不过的,就是存在上面的原因。

   一个典型的例子就是 我们创建一个 string 类型的 list 然后调用 toArray 方法发现返回的是一个 string[] 这时候自然就不能随便存放元素了。

  1. class A{
  2. }
  3. class B extends A {
  4. }
  5. public class JDKBug {
  6. @Test
  7. public void test1() {
  8. B[] arrB = new B[10];
  9. A[] arrA = arrB;
  10. arrA[0]=new A();
  11. }
  12. }

3. 修改方法分析

1. Set 方法

   这个方法也很简单 ,首先进行范围判断,然后就是直接更新下标即可。

  1. // 也没啥好说的就是,设置新值返回老值
  2. public E set(int index, E element) {
  3. rangeCheck(index);
  4. E oldValue = elementData(index);
  5. elementData[index] = element;
  6. return oldValue;
  7. }

2. Add(E e) 方法

  这个方法首先调用了 ensureCapacityInternal() 这个方法里面就判断了当前的 elementData 是否等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 如果是的话,就把数组的大小设置为 10 然后进行扩容操作,这里刚好解释了为什么采用无参构造的List 的大小是 10 ,这里扩容操作调用的方法是 ensureExplicitCapacity 里面就干了一件事如果用户指定的大小 大于当前长度就扩容,扩容的方法采用了 Arrays.copy 方法,这个方法实现原理是 new 出一个新的数组,然后调用 System.arraycopy 拷贝数组,最后返回新的数组。

  1. public boolean add(E e) {
  2. // 当调用了无参构造,设置大小为10
  3. ensureCapacityInternal(size + 1); // Increments modCount
  4. elementData[size++] = e;
  5. return true;
  6. }
  7. private void ensureCapacityInternal(int minCapacity) {
  8. // 如果当前数组是默认空数组就设置为 10和 size+1中的最小值
  9. // 这也就是说为什么说无参构造 new 的数组大小是 10
  10. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  11. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  12. }
  13. ensureExplicitCapacity(minCapacity);
  14. }
  15. private void ensureExplicitCapacity(int minCapacity) {
  16. modCount++;
  17. // 若用户指定的最小容量 > 最小扩充容量,则以用户指定的为准,否则还是 10
  18. if (minCapacity - elementData.length > 0)
  19. grow(minCapacity);
  20. }
  21. private void grow(int minCapacity) {
  22. // overflow-conscious code
  23. int oldCapacity = elementData.length;
  24. // 1.5倍增长
  25. int newCapacity = oldCapacity + (oldCapacity >> 1);
  26. if (newCapacity - minCapacity < 0)
  27. newCapacity = minCapacity;
  28. if (newCapacity - MAX_ARRAY_SIZE > 0)
  29. newCapacity = hugeCapacity(minCapacity);
  30. // minCapacity is usually close to size, so this is a win:
  31. elementData = Arrays.copyOf(elementData, newCapacity);
  32. }

3. Add(int index, E e) 方法

   这个方法比较简单和上面基本一样,然后只是最后放元素的时候的操作不一样,他是采用了 System.arraycopy 从自己向自己拷贝,目的就在于覆盖元素。 注意一个规律这里面只要涉及下标的操作的很多不是自己手写 for 循环而是采用类似的拷贝覆盖的方法。算是一个小技巧。

  1. public void add(int index, E element) {
  2. rangeCheckForAdd(index);
  3. ensureCapacityInternal(size + 1); // Increments modCount
  4. // 覆盖
  5. System.arraycopy(elementData, index, elementData, index + 1,
  6. size - index);
  7. elementData[index] = element;
  8. size++;
  9. }

4. remove(int index)

  同理这里面还是用了拷贝覆盖的技巧。 但是有一点注意的就是不用的节点需要手动的触发 gc ,这也是在 Efftive Java 中作者举的一个例子。

  1. public E remove(int index) {
  2. rangeCheck(index);
  3. modCount++;
  4. E oldValue = elementData(index);
  5. int numMoved = size - index - 1;
  6. //覆盖
  7. if (numMoved > 0)
  8. System.arraycopy(elementData, index+1, elementData, index,
  9. numMoved);
  10. elementData[--size] = null; // clear to let GC do its work
  11. return oldValue;
  12. }

5. remove(E e)

   这个方法操作很显然会判断 e 是不是 null 如果是 null 的话直接采用 == 比较,否则的话就直接调用 equals 方法然后执行拷贝覆盖。

  1. public boolean remove(Object o) {
  2. if (o == null) {
  3. for (int index = 0; index < size; index++)
  4. if (elementData[index] == null) {
  5. // 覆盖
  6. fastRemove(index);
  7. return true;
  8. }
  9. } else {
  10. for (int index = 0; index < size; index++)
  11. // 调用 equals 方法
  12. if (o.equals(elementData[index])) {
  13. fastRemove(index);
  14. return true;
  15. }
  16. }
  17. return false;
  18. }

6. clear()

   这个方法就干了一件事,把数组中的引用全都设置为 null 以便 gc 。而不是仅仅把 size 设置为 0 。

  1. // gc 所有节点
  2. public void clear() {
  3. modCount++;
  4. // clear to let GC do its work
  5. for (int i = 0; i < size; i++)
  6. elementData[i] = null;
  7. size = 0;
  8. }

7. addAll(Collection e)

   这个没啥好说的就是,采用转数组然后 copy

  1. // 一个套路 只要涉及到 Collection接口的方法都是把这个接口转成一个数组然后对数组操作
  2. public boolean addAll(Collection<? extends E> c) {
  3. Object[] a = c.toArray();
  4. int numNew = a.length;
  5. ensureCapacityInternal(size + numNew); // Increments modCount
  6. System.arraycopy(a, 0, elementData, size, numNew);
  7. size += numNew;
  8. return numNew != 0;
  9. }

4. 访问方法分析

1. get

   直接访问数组下标。

  1. // 没啥好说的直接去找数组下标
  2. public E get(int index) {
  3. rangeCheck(index);
  4. return elementData(index);
  5. }

2. subList

   这个方法的实现比较有意思,他不是直接截取一个新的 List 返回,而是在这个类的内部还有一个 subList 的内部类,然后这个类就记录了 subList 的开始结束下标,然后返回的是这个 subList 对象。你可能会想返回的 subList 他不是 List 不会有问题吗,这里这个 subList 是继承的 AbstractList 所以还是正确的。

  1. public List<E> subList(int fromIndex, int toIndex) {
  2. subListRangeCheck(fromIndex, toIndex, size);
  3. return new SubList(this, 0, fromIndex, toIndex);
  4. }
  5. // subList 返回的是一个位置标记实例,就是在原来的数组上放了一些标志,没有修改或者拷贝新的空间
  6. private class SubList extends AbstractList<E> implements RandomAccess {
  7. private final AbstractList<E> parent;
  8. private final int parentOffset;
  9. private final int offset;
  10. int size;
  11. // other functions .....
  12. }

5. 其他功能方法

1. write/readObject

  前面在介绍数据域的时候我就有标注 elementData 是一个 transition 的变量也就是在自动序列化的时候会忽略这个字段。

   然后我们又在源码中找到到了 write/readObject 方法,这两个方法是用来序列化 elementData 中的每一个元素,也就是手动的对这个字段进行序列化和反序列化。这不是多此一举吗?

   既然要将ArrayList的字段序列化(即将elementData序列化),那为什么又要用transient修饰elementData呢?

   回想ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。

   比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。

   所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。

  1. private void writeObject(java.io.ObjectOutputStream s)
  2. throws java.io.IOException{
  3. // Write out element count, and any hidden stuff
  4. int expectedModCount = modCount;
  5. s.defaultWriteObject();
  6. // Write out size as capacity for behavioural compatibility with clone()
  7. s.writeInt(size);
  8. // Write out all elements in the proper order.
  9. for (int i=0; i<size; i++) {
  10. s.writeObject(elementData[i]);
  11. }
  12. if (modCount != expectedModCount) {
  13. throw new ConcurrentModificationException();
  14. }
  15. }

2. fast-fail

   所谓的 fast-fail 就是在我们进行 iterator 遍历的时候不允许调用 Collection 接口的方法进行对容器修改,否则就会抛异常。这个实现的机制是在 iterator 中维护了两个变量,分别是 modCountexpectedModCount 由于 Collection 接口的方法在每次修改操作的时候都会对 modCount++ 所以如果在 iterator 中检测到他们不相等的时候就抛异常。

  1. private class Itr implements Iterator<E> {
  2. int cursor; // index of next element to return
  3. int lastRet = -1; // index of last element returned; -1 if no such
  4. int expectedModCount = modCount;
  5. final void checkForComodification() {
  6. if (modCount != expectedModCount)
  7. throw new ConcurrentModificationException();
  8. }
  9. }

3. forEach

   这个是一个函数式编程的方法,看看他的参数 forEach(Consumer<? super E> action) 很有意思里面接受是一个函数式的接口,我们里面回调了 Consumeraccept 所以我们只需要传入一个函数接口就能对每一个元素处理。

  1. @Override
  2. public void forEach(Consumer<? super E> action) {
  3. Objects.requireNonNull(action);
  4. final int expectedModCount = modCount;
  5. @SuppressWarnings("unchecked")
  6. final E[] elementData = (E[]) this.elementData;
  7. final int size = this.size;
  8. for (int i=0; modCount == expectedModCount && i < size; i++) {
  9. //回调
  10. action.accept(elementData[i]);
  11. }
  12. if (modCount != expectedModCount) {
  13. throw new ConcurrentModificationException();
  14. }
  15. }

写了一段测试代码,但是这个方法不常用,主要是 Collection 是可以自己生成 Stream 对象,然后调用上面的方法即可。这里提一下。

  1. public class ArrayListTest {
  2. @Test
  3. public void foreach() {
  4. ArrayList<Integer> list = new ArrayList<>();
  5. list.add(2);
  6. list.add(1);
  7. list.add(4);
  8. list.add(6);
  9. list.forEach(System.out::print); //打印每一次元素。
  10. }
  11. }

4. sort

底层调用了 Arrays.sort 方法没什么好说的。

  1. public void sort(Comparator<? super E> c) {
  2. final int expectedModCount = modCount;
  3. Arrays.sort((E[]) elementData, 0, size, c);
  4. if (modCount != expectedModCount) {
  5. throw new ConcurrentModificationException();
  6. }
  7. modCount++;
  8. }

5. removeIf

   这个和 forEach 差不多,就是回调写好了。

6. Vector

以上基本是把 ArrayList 的重要的方法和属性介绍完了,我们已经比较清楚他底层的实现和数据结构了。然后提到 ArrayList 自然也少不了一个比较古老的容器 Vector 这个容器真的和 ArrayList 太像了。因为你会发现他们连继承和实现的接口都是一样的。但是也会有一些不同的地方,下面分条介绍一下。

  1. Vector 中基本所有的方法都是 synchronized 的方法,所以说他是线程安全的 ArrayList

  2. 构造方法不一样,在属性中没有两个比较特殊的常量,所以说他的构造方法直接初始化一个容量为 10 的数组。然后他有四个构造方法。

  3. 遍历的接口不一样。他还是有 iterator 的但是他以前的遍历的方法是 Enumeration 接口,通过 elements 获取 Enumeration 然后使用 hasMoreElementsnextElement 获取元素。

  4. 缺少一些函数式编程的方法。

「必知必会」最细致的 ArrayList 原理分析的更多相关文章

  1. 「必知必会」最细致的 LinkedList 原理分析

    1.结构 1. 继承   该类继承自 AbstractSequentialList 这个是由于他是一个顺序的列表,所以说继承的是一个顺序的 List 2. 实现 这个类实现的接口比较多,具体如下: 首 ...

  2. Java并发必知必会第三弹:用积木讲解ABA原理

    Java并发必知必会第三弹:用积木讲解ABA原理 可落地的 Spring Cloud项目:PassJava 本篇主要内容如下 一.背景 上一节我们讲了程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单? ...

  3. SQL 必知必会

    本文介绍基本的 SQL 语句,包括查询.过滤.排序.分组.联结.视图.插入数据.创建操纵表等.入门系列,不足颇多,望诸君指点. 注意本文某些例子只能在特定的DBMS中实现(有的已标明,有的未标明),不 ...

  4. 《MySQL必知必会》整理

    目录 第1章 了解数据库 1.1 数据库基础 1.1.1 什么是数据库 1.1.2 表 1.1.3 列和数据类型 1.1.4 行 1.1.5 主键 1.2 什么是SQL 第2章 MySQL简介 2.1 ...

  5. 迈向高阶:优秀Android程序员必知必会的网络基础

    1.前言 网络通信一直是Android项目里比较重要的一个模块,Android开源项目上出现过很多优秀的网络框架,从一开始只是一些对HttpClient和HttpUrlConnection简易封装使用 ...

  6. SQL 必知必会 总结(一)

    SQL必知必会 总结(一) 第 1 课 了解SQL 1.数据库(database): 保存有组织的数据容器(通常是一个文件或一组文件). 2.数据库管理系统(DBMS): 数据库软件,数据库是通过 D ...

  7. MySQL必知必会1-20章读书笔记

    MySQL备忘 目录 目录 使用MySQL 检索数据 排序检索数据 过滤数据 数据过滤 用通配符进行过滤 用正则表达式进行搜索 创建计算字段 使用数据处理函数 数值处理函数 汇总数据 分组数据 使用子 ...

  8. 【MySQL 基础】MySQL必知必会

    MySQL必知必会 简介 <MySQL必知必会>的学习笔记和总结. 书籍链接 了解SQL 数据库基础 什么是数据库 数据库(database):保存有组织的数据的容器(通常是一个文 件或一 ...

  9. 读书笔记汇总 - SQL必知必会(第4版)

    本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...

随机推荐

  1. python 字典和列表嵌套用法

    python中字典和列表的使用,在数据处理中应该是最常用的,这两个熟练后基本可以应付大部分场景了.不过网上的基础教程只告诉你列表.字典是什么,如何使用,很少做组合说明. 刚好工作中采集promethe ...

  2. 环境安装——MySQL安装

    @ 目录 一文教会你安装与卸载MySQL 1. 官网下载 2. 文件下载 3. 卸载步骤 4. 安装步骤 5. MySQL配置 6. 安装出现的问题 6.1 如果出现了最后一步卡死的状态的话,不要慌: ...

  3. element的日期选择使用value-format之后表单验证报错

    在表单验证的时候报错 添加一个日期控件,但是发现在表单验证中遇到了冲突如下: Error in event handler for "el.form.change": " ...

  4. 6.11、制作windos虚拟机

    1.下载kvm支持windows系统的驱动程序: cd /tmp/ wget https://fedorapeople.org/groups/virt/virtio-win/direct-downlo ...

  5. Doris开发手记2:用SIMD指令优化存储层的热点代码

    最近一直在进行Doris的向量化计算引擎的开发工作,在进行CPU热点排查时,发现了存储层上出现的CPU热点问题.于是尝试通过SIMD的指令优化了这部分的CPU热点代码,取得了较好的性能优化效果.借用本 ...

  6. 使用Hugo框架搭建博客的过程 - 前期准备

    前言 这篇教程介绍了如何搭建这样效果的博客. 所需步骤 可以从这样的角度出发: 注册域名. 使用CDN加快网站访问速度. 网站内容需要部署在服务器或对象存储平台上. 重要的是放什么内容.博客需要选择框 ...

  7. Linux | 搜索命令

    grep grep 命令用于在文本中执行关键词搜索,并显示匹配的结果,格式:grep[选项][文本] grep命令的参数及其作用 参数 作用 -b 将可执行文件当作文本文件对待 -c 公显示找到的行数 ...

  8. C++ 标准模板库(STL)——迭代器(iterators)的用法及理解

    C++ STL中迭代器(iterators)用于遍历对象集合的元素.由于容器大小随着插入删除等操作动态改变,无法像静态数组那样获取数组长度然后遍历容器里的所有元素:这时就需要迭代器,每次从容器内第一个 ...

  9. python 爬取网络小说 清洗 并下载至txt文件

    什么是爬虫 网络爬虫,也叫网络蜘蛛(spider),是一种用来自动浏览万维网的网络机器人.其目的一般为编纂网络索引. 网络搜索引擎等站点通过爬虫软件更新自身的网站内容或其对其他网站的索引.网络爬虫可以 ...

  10. Java的锁升级策略

    什么是锁? java中,synchronized永远都是锁定的一个对象,那么jvm是怎么判断一个对象是被锁定的呢. java的对象内存分布 Java的对象由对象头,对象体和填充空间(Padding)组 ...