「必知必会」最细致的 ArrayList 原理分析
从今天开始也正式开 JDK 原理分析的坑了,其实写源码分析的目的不再是像以前一样搞懂原理,更重要的是看看他们编码风格更进一步体会到他们的设计思想。看源码前先自己实现一个再比对也许会有不一样的收获!
1. 结构
首先我们需要对 ArrayList 有一个大致的了解就从结构来看看吧.
1. 继承
该类继承自 AbstractList 这个比较好说
2. 实现
这个类实现的接口比较多,具体如下:
- 首先这个类是一个 List 自然有 List 接口
- 然后由于这个类需要进行随机访问,所谓随机访问就是用下标任一访问,所以实现了RandomAccess
- 然后就是两个集合框架肯定会实现的两个接口 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. 主要方法
下面的方法后面标有数字的就是表示重载方法
- ctor-3
- get
- set
- add-2
- remove-2
- clear
- addAll
- write/readObject
- fast-fail 机制
- subList
- iterator
- forEach
- sort
- 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
中维护了两个变量,分别是 modCount
和 expectedModCount
由于 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)
很有意思里面接受是一个函数式的接口,我们里面回调了 Consumer
的 accept
所以我们只需要传入一个函数接口就能对每一个元素处理。
@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
太像了。因为你会发现他们连继承和实现的接口都是一样的。但是也会有一些不同的地方,下面分条介绍一下。
在
Vector
中基本所有的方法都是synchronized
的方法,所以说他是线程安全的ArrayList
构造方法不一样,在属性中没有两个比较特殊的常量,所以说他的构造方法直接初始化一个容量为 10 的数组。然后他有四个构造方法。
遍历的接口不一样。他还是有
iterator
的但是他以前的遍历的方法是Enumeration
接口,通过elements
获取Enumeration
然后使用hasMoreElements
和nextElement
获取元素。缺少一些函数式编程的方法。
「必知必会」最细致的 ArrayList 原理分析的更多相关文章
- 「必知必会」最细致的 LinkedList 原理分析
1.结构 1. 继承 该类继承自 AbstractSequentialList 这个是由于他是一个顺序的列表,所以说继承的是一个顺序的 List 2. 实现 这个类实现的接口比较多,具体如下: 首 ...
- Java并发必知必会第三弹:用积木讲解ABA原理
Java并发必知必会第三弹:用积木讲解ABA原理 可落地的 Spring Cloud项目:PassJava 本篇主要内容如下 一.背景 上一节我们讲了程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单? ...
- SQL 必知必会
本文介绍基本的 SQL 语句,包括查询.过滤.排序.分组.联结.视图.插入数据.创建操纵表等.入门系列,不足颇多,望诸君指点. 注意本文某些例子只能在特定的DBMS中实现(有的已标明,有的未标明),不 ...
- 《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 ...
- 迈向高阶:优秀Android程序员必知必会的网络基础
1.前言 网络通信一直是Android项目里比较重要的一个模块,Android开源项目上出现过很多优秀的网络框架,从一开始只是一些对HttpClient和HttpUrlConnection简易封装使用 ...
- SQL 必知必会 总结(一)
SQL必知必会 总结(一) 第 1 课 了解SQL 1.数据库(database): 保存有组织的数据容器(通常是一个文件或一组文件). 2.数据库管理系统(DBMS): 数据库软件,数据库是通过 D ...
- MySQL必知必会1-20章读书笔记
MySQL备忘 目录 目录 使用MySQL 检索数据 排序检索数据 过滤数据 数据过滤 用通配符进行过滤 用正则表达式进行搜索 创建计算字段 使用数据处理函数 数值处理函数 汇总数据 分组数据 使用子 ...
- 【MySQL 基础】MySQL必知必会
MySQL必知必会 简介 <MySQL必知必会>的学习笔记和总结. 书籍链接 了解SQL 数据库基础 什么是数据库 数据库(database):保存有组织的数据的容器(通常是一个文 件或一 ...
- 读书笔记汇总 - SQL必知必会(第4版)
本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...
随机推荐
- 大型情感类技术连续剧-徒手撸一个 uTools(二)
前言 上篇手把手教你实现一个支持插件化的 uTools 工具箱我们介绍过了如何通过 electron 实现 utools 的插件功能体系,并按照 utools 的交互和设计做出了一套可以支持插件化的桌 ...
- To_Heart—题解——AT2165
这是一篇解题报告 首先,看到标签,考虑二分答案. 我们二分答案(即塔顶的值),把大于或等于这个值的变为1,否则变为0. 很容易发现,如果塔顶的答案是1,那么就说明值可以更大,否则相反. 复制一波样例 ...
- 解决List遍历删除元素提示ConcurrentModificationException
JDK1.8提供新的API ===> removeIf public static void main(String[] args) { List<String> list = ...
- Java:Java实例化(new)过程
实例化过程(new) 1.首先去JVM 的方法区中区寻找类的class对象,如果能找到,则按照定义生成对象,找不到 >>如下2.所示 2.加载类定义:类加载器(classLoader)寻找 ...
- Java程序设计(2021春)——第二章课后题(选择题+编程题)答案与详解
Java程序设计(2021春)--第二章课后题(选择题+编程题)答案与详解 目录 Java程序设计(2021春)--第二章课后题(选择题+编程题)答案与详解 第二章选择题 2.1 面向对象方法的特性 ...
- B站蹦了,关我A站什么事?
昨天的大瓜,B站蹦了,大伙都跳起来分析了一波异常原因,着实给大伙的秋招准备了一波热乎乎的素材!在大家都在关注 B站的时候, 我大A站终于要站起来了!!!经过多方网友的极力引流,我A站也蹦了- 紧急通知 ...
- Socket 编程介绍
Socket 编程发展 Linux Socket 编程领域,为了处理大量连接请求场景,需要使用非阻塞 I/O 和复用.select.poll 和 epoll 是 Linux API 提供的 I/O 复 ...
- DHCP工作原理
DHCP:Dynamic Host Configurtion Protocol DHCP的工作原理(UDP) 1.客户端:首先会发送给一个dhcp discovery(广播)报文,报文中的2层和3层都 ...
- Docker 基础备忘录
Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的.可移植的.自给自足的容器.开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机).bare metal. ...
- chown、chgrp 改变所有者、所属组
chown [option] [所有者][:[所属组]] file... chown指定文件的拥有者或者所属组,可以通过用户名或者用户id.组名.组id来修改,同时可以修改多个文件,文件以空格分割,支 ...