ArrayList分析1-循环、扩容、版本
ArrayList分析1-循环、扩容、版本
转载请注明出处 https://www.cnblogs.com/funnyzpc/p/16407733.html
前段时间抽空看了下ArrayList的源码,发现了一些有意思的东东,真的是大受裨益哈,尤其是版本问题
所以,本篇博客开始我将大概分三篇讲讲ArrayList里面一些有意思的点哈,由于源码大概一千八百逾行,里面大多代码都很通俗,也有些部分存在重复的(Itr以及SubList的内部方法),因为大多通俗遂这里不会逐行的分析哈,好了,现在开始~
一.关于循环的一个问题
首先,我给出一个很easy的循环:
public static void main(String[] args) {
for(int i = 0;i<8;i++){
System.out.print(i+"\t"); // 0 1 2 3 4 5 6 7
}
}
看起来很简单吧,哈哈,这时我会问:各位有没试过将i提到for循环外边呢,像下面这样:
public static void main(String[] args) {
int i;
for(i = 0;i<8;i++){
System.out.println(i);
}
System.out.println(i);// ?
}
上面第六行的i会输出什么呢?真是个有意思的问题,这真是一个微小而有意思的问题,我们经常使用,却很少利用for去做一些别样的事儿,ArrayList就有一骚操作,
原本我是准备臆测出来,却发现怎么也理解不了,当然啦,这个问题接下来我会说到:探究这个问题我们先看看一个普通的for循环的结构
for(定义1;定义2;定义3){
//定义4 :循环内的语句块
}
个人文采拙劣,这里就用定义一词哈
定义1: 这个地方我们经常会用int i=0; 这样一个语句,其实这个地方是对循环的变量做一次定义,这个地方的定义是一次性的,而且是第一次循环的时候会执行。
定义2: 这里一般是个判断性的表达式,而且这个地方的整体必须返回一个boolean,这个很重要,既然这个地方只需要返回一个布尔的结果,那么你想过没有,如果这个地方 我写的是 (i<10 && i>=0) 会不会抛错呢
定义4: 这是循环内语句块,通常我们会取到当前循环到的i进行某些逻辑处理,这里不是重点哈。
定义3: 这个地方是重点,一般我们会说每次循环后我们会将i--或者i++, 这种循环变量变化我们一般都会写在这个位置,这是_very very normal_的,但问题是每次执行完定义4的部分 就一定会执行定义3这个地方嘛? 答案是:一定会的!,为什么呢,看看生成的字节码指令就知道了哈 :
0 iconst_0
1 istore_1
2 iload_1
3 bipush 8
5 if_icmpge 21 (+16)
8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println : (I)V> //打印i
15 iinc 1 by 1
18 goto 2 (-16)
21 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
24 iload_1
25 invokevirtual #3 <java/io/PrintStream.println : (I)V> //打印i
28 return
以上是main函数内的完整字节码内容(jdk=java8), 可以看到指令内有两处println,自然第一个println即是for循环内的(标号12处的),下面一行就很重要了,官方描述是:将局部栈帧的索引+1(see: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc),说明白些也就是将i加一,然后就到了标号18这个位置,goto是将当前语句指向标号2这个位置 将store的i加载...到这里也就很明白了, goto指令是在i自增1之后,可以完全确认循环外的println打印的就一定是 8 !
看似简单的操作 ArrayList 则时常使用,比如可以用i循环,循环完成后,数组的大小不就是这个i了?以下ArrayList->Itr内的一段代码:
// 循环每个剩余操作
// 这是java8提供给iterator的函数式循环接口,其使用方式如下
// ArrayList arr = new ArrayList();
// arr.add("a");
// arr.add("b");
// arr.add("c");
// System.out.println(arr);
// Iterator iterator = arr.iterator();
// iterator.next(); // a
// iterator.forEachRemaining(item-> System.out.println(item)); // b c
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
// 检查是否为null,否则抛出错误
Objects.requireNonNull(consumer);
// 获取当前数组大小并检查迭代器的游标位置是否大于数组大小
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
// 老实说 elementData.length 与 ArrayList.this.size 是一对一关联的,这里这样做似乎多余
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
// 消费这个元素,同时将游标位置+1
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic 在迭代结束时更新一次以减少堆写入流量
// 因为i在以上已经+1了,所以这里直接赋值以及重置当前迭代的索引位置(lastRet)
cursor = i;
lastRet = i - 1;
checkForComodification();
}
上面这是迭代器内的部分迭代方法的定义,可以看到有句 cursor = i; 这个地方的i其实也就是size哈,稍不注意就理解错了,小问题让大家见笑了...
二.ArrayList扩容
ArrayList内部其实也就是维护了一个 Object 类型的数组,它具体是这样定义的transient Object[] elementData; ,看起来是不是超简单呢?呵呵呵,如果将ArrayList看作成一个大水缸的话,这个elementData就是水缸的本体,ArrayList可以看做一个用户态的接口,简单理解它其实就是是水缸外层刷的油漆或盖子抑或是漏斗。
打比方,如果这个水缸 (elementData )能装四桶水,那么这四桶水我们用 initialCapacity 这个变量来表示,当前实际上如果只有两桶水,则这个水缸实际储水容量(两桶水)我们用size来表示,这样就好理解了吧?
好了,如果预先知道将有8桶水倒入缸内,那我们就要准备一个能容纳8桶水的水缸,对于代码就是这样的:public ArrayList(int initialCapacity) {....}
当然,如果我们只允许用一个水缸来储存水的话,这个水缸当前如果是满载(8桶水),第9桶水的时候就需要准备一个至少能容纳9桶水的水缸,对于ArrayList来说这时候就需要扩容了,代码是这样的:
// 確保顯式容量(官方直译,不懂直接看代码)
private void ensureExplicitCapacity(int minCapacity) {
// 这个变量记录的是当前活动数组被修改的次数
// 每添加一个(准确的说是一次)元素修改次数+1,如果是addAll也算做是+1
modCount++;
// overflow-conscious code 判断是否溢出
// 可以简单的理解 minCapacity 为当前需要保证的最小容量(具体大小为当前容量+1:这是对于当前add元素的个数而定的),elementData.length则为当前活动数组的容量
// minCapacity 也为添加元素后所需数组容量大小,如果(所需容量)大于当前(添加前)数组容量即需要<b>扩容</b>
if (minCapacity - elementData.length > 0)
grow(minCapacity); //增长(扩容)
}
当然这时本着少腾挪多储水的原则,我们一般不会准备一个只能容纳9桶水的水缸,水缸太大也不好,容易浪费缸的容量维护也麻烦,所以对于ArrayList这个水缸,我们每次增长为现有容量的1.5倍(多了50%左右,如果当前Capacity是10->增长到15,9->增长到13),具体对应到ArrayList的代码就是:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 增加容量以確保它至少可以容納最小容量參數指定的元素數量。
* @param minCapacity the desired minimum capacity 所需的最小容量(也即当前需要的容量大小)
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 这里的增长策略是 oldCapacity=10 -> newCapacity=15 oldCapacity=9 -> newCapacity=14
// 即 每一次增长的为上一次的一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 这里个人觉得只是一个保险,对于类似addAll这样的操作 newCapacity 可能小于一次add的数量
// 比如当前容量是10[oldCapacity:10->newCapacity:15],addAll(100)后所需的容量还是不够 这时就会出现[newCapacity:100]
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 这里也是一个保险,对于待扩容后的大小比数组最大(MAX_ARRAY_SIZE)还要大的时候启用hugeCapacity(minCapacity)
// 这里调用 hugeCapacity 后顶多扩容8个大小 MAX_ARRAY_SIZE=2147483639(Integer.MAX_VALUE-8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 翻译: minCapacity 通常接近 size,所以這是一個勝利
// Arrays.copyOf:
// 複製指定的數組,截斷或填充空值(如有必要),使副本具有指定的長度。對於在原始數組和副本中都有效的所有索引,
// 這兩個數組將包含相同的值。對於在副本中有效但在原始副本中無效的任何索引,副本將包含 null。
// 當且僅當指定長度大於原始數組的長度時,此類索引才會存在。結果數組與原始數組的類完全相同。
elementData = Arrays.copyOf(elementData, newCapacity);
}
上面有一句很重要 : int newCapacity = oldCapacity + (oldCapacity >> 1); ,每次换用更大的水缸时都会将之前缸内的水兑过来,对应ArrayList就是这句:elementData = Arrays.copyOf(elementData, newCapacity);
算是很形象吧,这只是简单的理解,扩容也还有很多内容,比如什么时候扩容,扩容一个单位不够怎么办,满了怎么办???等等问题....
三.ArrayList中的版本管理
一开始大家会觉得这是个奇怪的问题,ArrayList中为啥会有版本,版本做什么用?
首先,我详细解答第一个问题:ArrayList中为什么有版本?,首先先看一段ArrayList的源码,关于Iterator的:
(以下为第一段代码)
public Iterator<E> iterator() {
// 虽然与都会返回一个迭代器,但是iterator只能单向循环,且不能实现增删改查
// 详见: https://www.cnblogs.com/tjudzj/p/4459443.html
return new Itr();
}
(以下为第二段代码)
public E next() {
// 版本检查
checkForComodification();
// 这个将游标赋予i,然后检查是否i是否超出当前数组索引位置(size)
// 我暂时没看出以下三行跟 hasNext() 有多少区别。。。,而且checkForComodification内也是做了安全检查了的
// 总结就是:十分没必要啊...
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
// 不大明白为啥要再整个引用 ,通过这个新引用索引返回数组值
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 因为迭代器每次循环前都会调用 hasNext ,故此推测这里也应该将游标+1
cursor = i + 1;
// 需要说明的是这个 lastRet 是个成员变量,而i只是个方法内临时变量而已
// 所以每循环一次这个 lastRet 需要记录为当前返值前的当前索引位置
return (E) elementData[lastRet = i];
}
(以下为第三段代码)
//// example:
// ArrayList arr = new ArrayList();
// arr.add("a");
// arr.add("b");
// arr.add("c");
// ListIterator listIterator = arr.listIterator();
// arr.remove("a");// throw error
// Object previous = listIterator.previous();
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
额,由于只是截取了部分代码,我先简单讲讲上面三段代码,第一段代码很明显,我们使用迭代器的时候首先调用的就是集合类的 iterator() 方法,而 iterator() 内部 只做了一件事儿:new Itr() ,现在知道迭代器是一个类对象,这点很重要。
继续看第二段:next()方法,这个方法内部第一行代码 是 checkForComodification(), 这是一个较为特殊的存在,点进去会看到以上第三段代码,if判断内有两个参数 ,一个是 modCount (直译为修改次数),第二个参数为 expectedModCount (预期修改的次数),点到这个参数定义,它只有一句很简单的定义int expectedModCount = modCount; ,是不是很迷糊?
在next()内还有一句也很重要 Object[] elementData = ArrayList.this.elementData; ,这句估计很好懂了,Itr迭代器内使用的数组其实也就是ArrayList中维护的数组对象(elementData),倒退一步,再往回思考下 checkForComodification()看...
不知读者老爷有没恍然大悟,其实很简单啦: Itr对象不希望你在使用Itr迭代器的过程中修改(主要是增删)ArrayList中的(elementData)元素,不然在迭代的时候源数组少了个元素会直接抛错的,Itr内的expectedModCount只会在 new Itr() 时被赋值一次,这就是很好的证明啦~
Itr是Iterator的实现,里面只有迭代的操作,如果有更复杂的操作,比如ListItr(是Itr以及ListIterator的继承实现) 里面更是对迭代器增加了增删改查方法,以及SubList这个对象内部也是,内部均是对ArrayList维护elementData直接操作(他们并未拷贝elementData),所以里面的增删操作不仅仅要比较ArrayList的elementData版本,也要在操作(增删)之后同步ArrayList的modCount版本,以下是ListItr内的add(E e)函数的源码:
public void add(E e) {
checkForComodification();
try {
// ArrayList的添加方法,在游标位置添加元素,游标及之后的元素往后移动,
int i = cursor;
// 这里还需要注意的是这个插入是在当前元素之后插入元素,ArrayList则是在元素之前,这主要是游标是当前位置+1
ArrayList.this.add(i, e);
// 因为增加了个元素,所以游标的位置要+1,当前位置lastRet会在下一次调用next或previous时会被重置
cursor = i + 1;
lastRet = -1;
// 这个其实类似于版本的概念,主要由于ArrayList与Iterator修改不同步,expectedModCount,这个会在 checkForComodification() 中进行校验
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
以上代码中有这样一句 expectedModCount = modCount; 不知读者们明白否。。。
ArrayList中为啥有版本管理,版本管理怎么用?
好了,我总结下本小节开头的的两个问题
首先版本管理就是在增删元素的时候对 modCount 自增1
因为对ArrayList的迭代器 Itr、ListItr以及SubList(截取类) 他们是单独类对象同时内部也是直接操作的ArrayList的源elementData数组对象,所以在ArrayList添加元素时这三个类内部方法均不知道数组元素个数已发生变化,所以在操作elementData时候均需要判读版本是否一致,这就是为啥有版本;
他解决的是:这几个类在操作 elementData (ArrayList的)时 ArrayList可能对其的增删导致的版本不一致的问题,总结似乎臭长了些,但就是这么个意思
理解这个很重要,不然你在读 ArrayList的 add 、remove这类方法时不知modCount作甚云云,哈哈。。。
ArrayList分析1-循环、扩容、版本的更多相关文章
- Java ArrayList源码分析(含扩容机制等重点问题分析)
写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解.所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项 ...
- ArrayList分析2 :Itr、ListIterator以及SubList中的坑
ArrayList分析2 : Itr.ListIterator以及SubList中的坑 转载请注明出处:https://www.cnblogs.com/funnyzpc/p/16409137.html ...
- Spring源码分析之循环依赖及解决方案
Spring源码分析之循环依赖及解决方案 往期文章: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostPro ...
- Java源码之ArrayList分析
一.ArrayList简介 ArrayList底层的数据结构是数组,数组元素类型为Object类型,即可以存放所有类型数据. 与Java中的数组相比,它的容量能动态增长.当创建一个数组的时候,就必须确 ...
- 竟然还有人说ArrayList是2倍扩容,今天带你手撕ArrayList源码
ArrayList是我们开发中最常用到的集合,但是很多人对它的源码并不了解,导致面试时,面试官问的稍微深入的问题,就无法作答,今天我们一起来探究一下ArrayList源码. 1. 简介 ArrayLi ...
- Java集合源码学习(二)ArrayList分析
>>关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫&qu ...
- ArrayList分析
ArrayList概述 ArrayList继承了AbstractList,实现了List接口,底层基于动态数组,容量大小可以动态变化,ArrayList中可以添加null元素,另外,ArrayList ...
- 集合ArrayList分析
目录 ArrayList 描述 重要的对象 遍历使用 与Collection关系 ArrayList属性 扩展:什么是序列化 transient关键字解析 ArrayList构造方法 无参构造 int ...
- HashMap源码解析、jdk7和8之后的区别、相关问题分析(多线程扩容带来的死循环)
一.概览 HashMap<String, Integer> map = new HashMap<>(); 这个语句执行起来,在 jdk1.8 之前,会创建一个长度是 16 的 ...
随机推荐
- 聊聊buffer和cache的区别以及是什么?
buffer 众所周知,想把数据写入磁盘,肯定要先把数据文件读到内存中,当修改完这个文件时,不会立即写入磁盘,为了减少磁盘IO,提高性能,所有会留存一段时间再写入磁盘,这就是buffer cache ...
- sentinel基础概念及使用
点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. 什么是sentinel sentinel是Spring Cloud Al ...
- Halo 开源项目学习(四):发布文章与页面
基本介绍 博客最基本的功能就是让作者能够自由发布自己的文章,分享自己观点,记录学习的过程.Halo 为用户提供了发布文章和展示自定义页面的功能,下面我们分析一下这些功能的实现过程. 管理员发布文章 H ...
- FreeRTOS --(2)内存管理 heap1
转载自https://blog.csdn.net/zhoutaopower/article/details/106631237 FreeRTOS 提供了5种内存堆管理方案,分别对应heap1/heap ...
- 神经网络 CNN 名词解释
隐藏层 不是输入或输出层的所有层都称为隐藏层. 激活和池化都没有权重 使层与操作区分开的原因在于层具有权重.由于池操作和激活功能没有权重,因此我们将它们称为操作,并将其视为已添加到层操作集合中. 例如 ...
- java高级用法之:JNA中的回调
目录 简介 JNA中的Callback callback的应用 callback的定义 callback的获取和应用 在多线程环境中使用callback 总结 简介 什么是callback呢?简单点说 ...
- jQuery操作标签,jQuery事件操作,jQuery动画效果,前端框架
jQuery操作标签 jQuery代码查找标签绑定的变量名推荐使用 $xxxEle 样式类操作 addClass();// 添加指定的CSS类名. removeClass();// 移除指定的CSS类 ...
- Java类包
学习内容:Java类包 一.Java类包 1.一个完整的类名需要包名和类名的组合,每一个类都隶属于一个包. 例:完整类名--java.sql.Date 2.同一个包中类相互访问时可以不指明包名. 3. ...
- 理解RESTful Api设计
REST REST(REpresentational State Transfer)是 Roy Fielding 博士于 2000 年在他的博士论文中提出来的一种软件架构风格(一组架构约束条件和原则) ...
- Golang可重入锁的实现
Golang可重入锁的实现 项目中遇到了可重入锁的需求和实现,具体记录下. 什么是可重入锁 我们平时说的分布式锁,一般指的是在不同服务器上的多个线程中,只有一个线程能抢到一个锁,从而执行一个任务.而我 ...