ArrayList 源码分析

1. 结构

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

1. 继承

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

2. 实现

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

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

3. 主要字段

    // 默认大小为10
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认的空数组 这个是在传入无参的是构造函数会调用的待会再 add 方法中会看到
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 用来存放 ArrayList 中的元素 注意他的修饰符是一个 transient 也就是不会自动序列化
transient Object[] elementData;
// 大小
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 这个空数组。

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

2. 传入数组大小的构造

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

// 按传入的参数创建新的底层数组
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

3. 传入 Collection 接口

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

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

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

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

class A{
} class B extends A {
} public class JDKBug { @Test
public void test1() {
B[] arrB = new B[10];
A[] arrA = arrB;
arrA[0]=new A();
}
}

3. 修改方法分析

1. Set 方法

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

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

2. Add(E e) 方法

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

    public boolean add(E e) {
// 当调用了无参构造,设置大小为10
ensureCapacityInternal(size + 1); // Increments modCount
elementData[size++] = e;
return true;
} private void ensureCapacityInternal(int minCapacity) {
// 如果当前数组是默认空数组就设置为 10和 size+1中的最小值
// 这也就是说为什么说无参构造 new 的数组大小是 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
} ensureExplicitCapacity(minCapacity);
} private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 若用户指定的最小容量 > 最小扩充容量,则以用户指定的为准,否则还是 10
if (minCapacity - elementData.length > 0)
grow(minCapacity);
} private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 1.5倍增长
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

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

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

public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount
// 覆盖
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

4. remove(int index)

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

public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
//覆盖
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}

5. remove(E e)

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

public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 覆盖
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 调用 equals 方法
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

6. clear()

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

// gc 所有节点
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}

7. addAll(Collection e)

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

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

4. 访问方法分析

1. get

   直接访问数组下标。

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

2. subList

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

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

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方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

2. fast-fail

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

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

3. forEach

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

    @Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
//回调
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

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

public class ArrayListTest {

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

4. sort

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

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

5. removeIf

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

6. Vector

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

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

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

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

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

ArrayList 源码分析的更多相关文章

  1. Java集合干货——ArrayList源码分析

    ArrayList源码分析 前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然.关于ArrayList的具体 ...

  2. ArrayList源码分析超详细

    ArrayList源码分析超详解 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 IDEA 找到所需要分析的类(ztrl+N查找ArraLi ...

  3. Java - ArrayList源码分析

    java提高篇(二一)-----ArrayList 一.ArrayList概述 ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的.实现了所有可选列表操作,并允许包括 nul ...

  4. java集合系列之ArrayList源码分析

    java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...

  5. ArrayList源码分析超详细(转载)

    ArrayList源码分析超详细   ArrayList源码分析超详解 想要分析下源码是件好事,但是如何去进行分析呢?以我的例子来说,我进行源码分析的过程如下几步: 找到类:利用 IDEA 找到所需要 ...

  6. ArrayList源码分析--jdk1.8

    ArrayList概述   1. ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合.  2. ArrayList支持随机访问.克隆.序列化,元素有序且可以重复.  3. ...

  7. Java ArrayList源码分析(有助于理解数据结构)

    arraylist源码分析 1.数组介绍 数组是数据结构中很基本的结构,很多编程语言都内置数组,类似于数据结构中的线性表 在java中当创建数组时会在内存中划分出一块连续的内存,然后当有数据进入的时候 ...

  8. Java入门系列之集合ArrayList源码分析(七)

    前言 上一节我们通过排队类实现了类似ArrayList基本功能,当然还有很多欠缺考虑,只是为了我们学习集合而准备来着,本节我们来看看ArrayList源码中对于常用操作方法是如何进行的,请往下看. A ...

  9. Java ArrayList源码分析(含扩容机制等重点问题分析)

    写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解.所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项 ...

随机推荐

  1. R实战 第三篇:数据处理(基础)

    数据结构用于存储数据,不同的数据结构对应不同的操作方法,对应不同的分析目的,应选择合适的数据结构.在处理数据时,为了便于检查数据对象,可以通过函数attributes(x)来查看数据对象的属性,str ...

  2. AttributeError: 'TestLogin' object has no attribute 'driver' in Pycharm for python selenium

    自动化测试学习中的问题: 最近几天在写登陆测试,遇到一个问题,困惑我的几个小时......... 我各种百度,花费大量时间,才找到我的问题的根本所在,最终解决了我的问题,主要是大小写的问题def Se ...

  3. 笔记︱范数正则化L0、L1、L2-岭回归&Lasso回归(稀疏与特征工程)

    机器学习中的范数规则化之(一)L0.L1与L2范数 博客的学习笔记,对一些要点进行摘录.规则化也有其他名称,比如统计学术中比较多的叫做增加惩罚项:还有现在比较多的正则化. -------------- ...

  4. vc++怎么可以直接刷掉MBR?搞笑的吧

    FILE * fd=fopen("\\\\.\\PHYSICALDRIVE0","rb+"); char buffer[512]; fread(buffer,5 ...

  5. linux中mysql命令方式备份数据的问题

    这几天公司新出了个组件化的项目,里面需要用到mysql数据库相关的技术,由于之前一直用的mongodb,所以mysql几乎忘光了,于是只能在linux虚拟机中重新开始学习. 基本的增删改查还好,但是在 ...

  6. linux虚拟机中安装mongodb

    今天在linux虚拟机中安装了mongodb数据库,安装过程主要参照了网上的一片文档,大部分地方都没有多大区别,只是在某些细节上有所改变. 我参考的文档是:http://my.oschina.net/ ...

  7. CSS3的[att$=val]选择器

    1.实例源码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www. ...

  8. weblogic部署web项目出现错误

    1.错误描述 <2015-3-15 下午02时13分01秒 CST> <Info> <Security> <BEA-090905> <Disabl ...

  9. arttemplate与webpack的应用

    模板渲染使用arttemplate,使用方法如下: 普通使用 普通使用把渲染模板写在<script>标签里面,引入arttemplate.js,从服务端接收数据(数组与对象的形式),然后调 ...

  10. 使用promise方式写settimeout

    //使用promise方式写settimeout, //好处就是用于写动画的时候只需知道后一个的动画在前一个动画结束后多久执行 console.time('settimeout:');//开始计算这段 ...