ArrayList 源码分析 -- 扩容问题及序列化问题
文章目录
- 一、前言
- 二、ArrayList 的继承与实现关系
- 2.1 ArrayList.java
- 2.2 抽象类AbstractList.java
- 2.3 接口List.java
- 2.4 接口RandomAccess.java
- 2.5 接口Cloneable
- 2.6 接口Serializable
- 三、ArrayList 关于数组和集合的讨论
- 3.1 ArrayList 是数组还是集合问题说明
- 3.2 从构造方法分析ArrayList
- 3.1 确认ArrayList 是集合
- 四、ArrayList 初始容量是0 还是10 问题的确认
- 4.1 从构造方法看初始容量
- 4.2 从add() 方法看初始容量
- 4.3 确定ArrayList 的初始容量
- 五、ArrayList 的扩容问题探索
- 5.1 扩容问题说明
- 5.2 通过add() 方法探索扩容问题
- 5.3 扩容算法
- 5.4 模拟扩容演示
- 六、ArrayList 的序列化问题补充
一、前言
这里主要研究到以下问题,通过源码阅读分析探索以下问题的答案。本文不牵涉到更多问题,所以源码只贴出与这些问题直接联系的关键代码块。当然源码中必要的全局常量、方法会贴出。
- ArrayList 的继承与实现关系;
- ArrayList 关于数组和集合的讨论;
- ArrayList 初始容量是0还是10问题的确认;
- ArrayList 的扩容问题探索;
- ArrayList 的序列化问题补充;
二、ArrayList 的继承与实现关系
2.1 ArrayList.java
ArrayList类通过extends关键字继承AbstractList抽象类,通过关键字implements实现List集合接口、RandomAccess标记接口、Cloneable克隆接口、Serializable序列化接口。
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
图2-1、继承实现关系图
2.2 抽象类AbstractList.java
抽象类AbstractList继承一个AbstractCollection集合同样实现了集合List接口。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {}
2.3 接口List.java
List直接继承于底层集合Collection,List是一个集合,谁赞成?谁反对?
public interface List<E> extends Collection<E> {}
2.4 接口RandomAccess.java
此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
接口RandomAccess 是一个标记接口,实现该接口的集合List 支持快速随机访问。List 集合尽量要实现RandomAccess 接口,如果集合类是RandomAccess 的实现,则尽量用for(int i = 0; i < size; i++) 来遍历效率高,而不要用Iterator迭代器来遍历(如果List是Sequence List,则最好用迭代器来进行迭代)。
2.5 接口Cloneable
关于深拷贝与浅拷贝应写一篇博客去说明。想深入了解可以参考知乎问答深拷贝与浅拷贝
实现接口的目的是重写java.lang.Object的clone()的方法,实现浅拷贝。深拷贝和浅拷贝针对像 Object, Array 这样的复杂对象的。浅拷贝只复制一层对象的属性,而深拷贝则递归复制了所有层级。
- 浅拷贝
被复制(拷贝)对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。浅拷贝即新建一个对象,复制原对象的基本属性,一级属性到新的存储空间,不拷贝原对象的对象引用元素,新对象的对象引用指向原来的存储空间,修改对象引用的元素,那么拷贝对象和原对象都会变化。- 深拷贝
深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。深拷贝新建一个对象,不仅拷贝对象,还拷贝对象引用;深拷贝就是我们平常理解的复制,将对象的全部属性及对象引用复制到新的存储空间,不会指向原来的对象,修改新对象的任意元素都不会影响原对象。
2.6 接口Serializable
该接口无继承实现关系,实现该接口的类支持序列化。因此ArrayList 支持序列化。
- 序列化:可以将一个对象的状态写入一个Byte 流里;
- 反序列化:可以从其它地方把该Byte 流里的数据读出来。
三、ArrayList 关于数组和集合的讨论
3.1 ArrayList 是数组还是集合问题说明
ArrayList 是数组还是集合,这也算问题?
我们都知道List 是集合啊,ArrayList 继承于List 也是集合。不过你或许会在某些文章上见过ArrayList 是数组或者说ArrayList 是基于数组的说法。
3.2 从构造方法分析ArrayList
那我们首先看一下ArrayList 的构造方法;
// 默认空数组,final 关键字修饰private static final Object[] EMPTY_ELEMENTDATA = {};// 空数据的共享空数组实例private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** 存储ArrayList元素的数组缓冲区,即ArrayList 存放数据的地方* ArrayList的容量是这个数组缓冲区的长度* transient 关键字修饰,elementData 不支持序列化*/transient Object[] elementData;/*** 默认无参构造方法*/public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/*** 带整型参数的构造方法* @param initialCapacity 初始化容量大小*/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);}}/*** 泛型集合参数构造方法* @param c 集合类型参数*/public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// replace with empty array.this.elementData = EMPTY_ELEMENTDATA;}}
无参构造方法中直接初始化Object[] elementData = {}; 即ArrayList 的存储数据的容器是一个数组。带参构造方法也是new Object() 或者通过Arrays 的方法转换为数组对象。
ArrayList 实现了List 接口,重写了集合的add(),size(),get(),remove(),toArray()等方法,多个方法的内部代码块是基于数组来处理数据的。
3.1 确认ArrayList 是集合
因此ArrayList 是实现List 接口的集合,是基于数组的集合,数据的存储容器是数组,集合方法是通过数组实现的(比如泛型参数构造方法是将传入的集合c 先转化为数组在进行处理的)。包括其内部类Itr implements Iterator 中重新Iterator 的方法也是基于数组计算的。
搞这个问题有意义吗?有意义_
四、ArrayList 初始容量是0 还是10 问题的确认
4.1 从构造方法看初始容量
从第三部分中的构造方法可以看出
无参构造一个ArrayList 时存储数据的容器elementData = {};此时存储容器大小为0 ;
带整型参数的构造方法通过传入的整型数据的大小来确认初始化存储容器elementData 的大小,当initialCapacity == 0 时,还是赋值elementData = {};
泛型集合参数构造方法,根据集合的大小来初始化elementData 的大小,将集合转化为数组,数组的大小为0 的情况下,仍然赋值elementData = {};
4.2 从add() 方法看初始容量
这个初始化和10 又有什么关系???
在ArrayList 中定义了一个默认的存储容器的大小DEFAULT_CAPACITY 为10,用关键字final 修饰,注释是默认初始容器大小,通过构造方法创建ArrayList 对象并没有使用到这个常量,我们看看这个初始容器大小是怎么初始化容器大小的。
/*** Default initial capacity.*/private static final int DEFAULT_CAPACITY = 10;// 集合的逻辑大小,即存储真实数据的数量private int size;public int size() {return size;}public boolean isEmpty() {return size == 0;}/*** 添加元素* @param e* @return*/public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true;}/*** 确认集合内部容量大小* @param minCapacity*/private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}/*** 计算集合的容量* @param elementData* @param minCapacity* @return*/private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}
在集合add() 添加元素时,会将当前的size + 1 传入ensureCapacityInternal() 方法确认当前elementData 数组大小是否足够
足够的话size自增一,size = size + 1直接添加的元素赋值给elementData[size];
不足够的话进行扩容,扩容问题下面涉及,这里说扩容中特殊情况,对空集合的扩容,比如我们通过无参构造方法创建了集合对象,此时容器大小为0,然后调用add() 方法添加一个元素,此时elementData == {}即此时elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,满足该条件在计算集合的容量方法calculateCapacity 中会进行"容器初始化",其实是扩容而已;
这里的"=" 是等于不是赋值
此时return Math.max(DEFAULT_CAPACITY, minCapacity);
minCapacity = size + 1 = 0 + 1 = 1
DEFAULT_CAPACITY = 10
minCapacity < DEFAULT_CAPACITY = 1 < 10
结果return 10;
此时容器elementData 扩容为Object[10]
4.3 确定ArrayList 的初始容量
从以上两方面分析,所以ArrayList 的初始容量根据传参确定,默认无参构造方法下新对象的容器初始大小为0。而10 是在空集合添加第一个元素时扩容时的默认容器大小。
五、ArrayList 的扩容问题探索
5.1 扩容问题说明
集合扩容就是集合容量大小不能满足需要存储数据的数量,而需要将elementData 容器大小增大,以存储更多的元素。
5.2 通过add() 方法探索扩容问题
集合存储容器elementData 的容量大小不小于真实存储元素数量size
elementData.length > size 为真true
elementData.length = size 为真true
elementData.length < size 为假false
集合在添加元素时会首先判断当前容器是否能装下第size + 1 个元素。不能的情况下会进行扩容,上面初始容量问题中谈到当空集合扩容时会给该集合对象一个默认的容器大小10,即扩容到elementData.length == 10
这是一种特殊情况,给了一个默认值,并没有真正涉及扩容核心算法。
下面看看ArrayList 是如何扩容的。
// 集合最大容量private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** 添加元素* @param e* @return*/public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true;}/*** 确认集合内部容量大小* @param minCapacity 添加元素后容器的最小容量*/private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}/*** 计算集合的容量* @param elementData 存储数据的容器* @param minCapacity 添加元素后容器的最小容量* @return*/private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}/*** 确认明确的容量大小* @param minCapacity 添加元素后容器的最小容量*/private void ensureExplicitCapacity(int minCapacity) {modCount++;if (minCapacity - elementData.length > 0)grow(minCapacity);}/*** 扩容方法* @param minCapacity 添加元素后容器的最小容量*/private void grow(int minCapacity) {// 扩容前容器大小int oldCapacity = elementData.length;// 扩容关键算法,newCapacity 扩容后容器大小int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0) newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);// 将扩容后的容器赋值给存储容器elementData = Arrays.copyOf(elementData, newCapacity);}/*** 溢出处理*/private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) throw new OutOfMemoryError();// 超过最大值不合法,直接将容量大小定义为Intager 的最大值return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}
在添加元素的方法中依此调用容器大小判断相关的方法,当容器大小不够时,会进行扩容,调用grow() 方法进行扩容。扩容方法很简单,拿到扩容前容器大小oldCapacity,进行扩容,判断扩容后容量是否合法,是否溢出,然后进行处理为合理的大小。
5.3 扩容算法
扩容算法是首先获取到扩容前容器的大小。然后通过oldCapacity + (oldCapacity >> 1) 来计算扩容后的容器大小newCapacity。
这里的扩容算法用到了>> 右移运算。即将十进制转换为二进制,每一位右移后得到的结果。oldCapacity >> 1即oldCapacity 对2 求摩,oldCapacity/2;
oldCapacity + (oldCapacity >> 1)即oldCapacity + (oldCapacity / 2)
所以关键扩容算法就是当容量不够存储元素时,在原容器大小size 基础上再扩充size 的接近一半,即大约扩充原容器的一半。
相对直白的严谨的扩容算法如下:
扩容后容器大小newCapacity = size + size / 2
5.4 模拟扩容演示
举个栗子:原容器是10,elementData 已经存储10 个元素了,再次调用add() 方法会走grow() 方法进行扩容。运行中截图如下图
10 / 2 =5
新的容器大小为 10 + 5 = 15
另:运算 “/” 的结果是整数,15/2 =7;9/2 = 4; 8/2 = 4;
图5-1、扩容前容量大小图
图5-2、扩容后容量大小图
图5-3、扩容后elementData 容量大小图
六、ArrayList 的序列化问题补充
集合的存储容器elementData 使用transient 关键字修饰不支持序列化,但是我们知道ArrayList 是支持序列化的,那我们是怎么序列化集合中的数据呢,这里不直接序列化elementData,而是遍历每个数据分别进行IO 流处理来实现存储容器中对象的序列化的。
// ArrayList 列表结构被修改的次数。 protected transient int modCount = 0;private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException{// ArrayList 列表结构被修改的次数。int expectedModCount = modCount;s.defaultWriteObject();s.writeInt(size);// 对每一个对象进行IO 流的写处理for (int i=0; i<size; i++) {s.writeObject(elementData[i]);}if (modCount != expectedModCount) {throw new ConcurrentModificationException();}}
这里对存储容器Object[] elementData 用transient 关键字修饰,考虑到容器的存储空间在扩容后会产生很大闲置空间,扩容前容量越大这个问题越明显;序列化时会将空的对象空间也进行序列化,而真实存储的元素的数量为size,那样处理的话效率很低,所以这里不支持存储容器直接序列化,而写一个新的方法来只序列化size 个真实元素即可。
感谢您花费时间阅读这篇博客,也愿它能帮助到你
Power By niaonao, The End, Thanks
ArrayList 源码分析 -- 扩容问题及序列化问题相关推荐
- Java集合Collection源码系列-ArrayList源码分析
Java集合系列-ArrayList源码分析 文章目录 Java集合系列-ArrayList源码分析 前言 一.为什么想去分析ArrayList源码? 二.源码分析 1.宏观上分析List 2.方法汇 ...
- ArrayList 源码分析
公众号原文:ArrayList 源码分析 博客原文:ArrayList 源码分析 以下源码分析使用的 Java 版本为 1.8 1. 概览 ArrayList 是基于数组实现的,继承 Abstract ...
- 【Java源码分析】Java8的ArrayList源码分析
Java8的ArrayList源码分析 源码分析 ArrayList类的定义 字段属性 构造函数 trimToSize()函数 Capacity容量相关的函数,比如扩容 List大小和是否为空 con ...
- ArrayList源码分析与手写
本节主要分析JDK提供的ArrayList的源码,以及与自己手写的ArrayList进行对比. ArrayList源码分析 构造方法 private static final int DEFAULT_ ...
- 扩容是元素还是数组_02 数组(附ArrayList源码分析)
定义 用一组连续的内存空间存储一组具有相同类型的数据的线性表数据结构. 优势 支持通过下标快速的随机访问数据,时间复杂度为O(1). 劣势 通常情况下,插入和删除效率低下,每次操作后,需要进行后续元素 ...
- 面试必会之ArrayList源码分析手写ArrayList
作者:Java知音-微笑面对生活 简介 ArrayList是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储任意类型的数据,ArrayList是线程不安全的,非常适合用于 ...
- Java中ArrayList源码分析
一.简介 ArrayList是一个数组队列,相当于动态数组.每个ArrayList实例都有自己的容量,该容量至少和所存储数据的个数一样大小,在每次添加数据时,它会使用ensureCapacity()保 ...
- Java源码详解五:ArrayList源码分析--openjdk java 11源码
文章目录 注释 类的继承与实现 构造函数 add操作 扩容函数 remove函数 subList函数 总结 本系列是Java详解,专栏地址:Java源码分析 ArrayList 官方文档:ArrayL ...
- java list addall源码_Java集合:ArrayList源码分析
其实我看到已有很多大佬写过此类文章,并且写的也比较清晰明了,那我为何要再写一遍呢?其实也是为了加深本身的印象,巩固本身的基础html (主要是不少文章没有写出来我想知道的东西!!!!!!!)java ...
- djangorestframework源码分析2:serializer序列化数据的执行流程
djangorestframework源码分析 本文环境python3.5.2,djangorestframework (3.5.1)系列 djangorestframework源码分析-serial ...
最新文章
- [AX]AX2012 纪录缓存
- OpenGL ES 的例子
- No-5.变量的命名
- OS X 10.11 安装Cocoapods
- Docker的一些理解(二)
- oracle如何链接到另外一个数据库DB_LINK
- Oracle往表里插入系统当前时间
- Matlab2017b配置C++/C/Fortan编译器的问题
- 启动solidworks时显示VBE6EXT.OLB不能被加载
- pzh-web前端学习汇总-大二
- 朱啸虎建议创业者忘记区块链,遭应书岭回讽:你老了
- 关于ADB需要知道的一些知识
- 前端手把手教你js实现附件预览和下载得功能实现
- uni-app使用map组件开发map地图,获取后台返回经纬度进行标点
- 百趣代谢组学文献分享 | 建立基于代谢组学的ICU脓毒症患者预后预测模型
- 计算机全屏显示快捷键,最全电脑快捷键,电脑全屏按哪个键 原来是这样的
- 再次启航,留下每一步脚印
- vivado原语BUFHCE
- 【Java实现】P1249最大乘积 -落谷
- TF2.0 TFRecord创建和读取
热门文章
- scala_day01_安装_基础_IO_函数_递归_异常_方法_样例类_伴生对象
- Java 基础知识总结—String 类
- 电脑上有什么类似全能扫描王的软件?这4款扫描app1分钟帮你搞定几十张图片
- 【机器学习】有监督学习,无监督学习,半监督学习和强化学习
- 巧用 paypal 实现转账及网上安全支付
- 华氏和摄氏温度的转换
- 从pdf简历中提取信息——BiLSTM-CRF
- 无锡市计算机表演大赛,第二十七届中国儿童青少年计算机表演赛无锡赛区决赛-无锡少年宫.DOC...
- 小众创客的狂欢——树莓派
- 怎么安装linux和win10双系统,在Win10下安装Linux双系统的方法