JDK源码分析(3)之 ArrayList 相关
ArrayList
的源码其实比较简单,所以我并没有跟着源码对照翻译,文本只是抽取了一些我觉得有意思或一些有疑惑的地方分析的。
一、成员变量
private static final int DEFAULT_CAPACITY = 10; // 默认容量
private static final Object[] EMPTY_ELEMENTDATA = {}; // 空实例的空数组对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 也是空数组对象,用于计算添加第一个元素时要膨胀多少
transient Object[] elementData; // 存储内容的数组
private int size; // 存储的数量
其中elementData
被声明为了transient
,那么ArrayList是如何实现序列化的呢?
查看writeObject
和readObject
的源码如下:
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();
}
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
可以看到在序列化的时候是把elementData
里面的元素逐个取出来放到ObjectOutputStream
里面的;而在反序列化的时候也是把元素逐个拿出来放回到elementData
里面的;
这样繁琐的操作,其中最重要的一个好处就是节省空间,因为elementData
的大小是大于ArrayList
中实际元素个数的。所以没必要将elementData
整个序列化。
二、构造函数
ArrayList
的构造函数主要就是要初始化elementData
和size
,但是其中有一个还有点意思
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;
}
}
可以看到在Collection.toArray()
之后又判断了他的Class
类型是不是Object[].class
,这个也注释了是一个bug,那么在什么情况下会产生这种情况呢?
// test 6260652
private static void test04() {
List<String> list = Arrays.asList("111", "222", "333");
System.out.println(list.getClass());
Object[] objects = list.toArray();
System.out.println(objects.getClass());
}
打印:
class java.util.Arrays$ArrayList
class [Ljava.lang.String;
这里可以看到objects
的class
居然是[Ljava.lang.String
,同时这里的ArrayList
是java.util.Arrays.ArrayList
// java.util.Arrays.ArrayList
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable {
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
...
}
从以上例子可以看到,由于直接将外部数组的引用直接赋值给了List内部的数组,所以List所持有的数组类型是未知的。之前讲过数组是协变的,不支持泛型,所以只有值运行时再知道数组的具体类型,所以导致了以上的bug;难怪《Effective Java》里面讲数组的协变设计,不是那么完美。
三、常用方法
由于ArrayList
是基于数组的,所以他的api基本都是基于System.arraycopy()
实现的;
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
可以看到这是一个native
方法,并且JVM有对这个方法做特殊的优化处理,
private static void test05() {
int[] s1 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] s2 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.arraycopy(s1, 3, s2, 6, 2);
System.out.println(Arrays.toString(s1));
System.out.println(Arrays.toString(s2));
}
打印:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 4, 5, 9]
private static void test06() {
int[] s1 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] s2 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.arraycopy(s1, 3, s2, 6, 5);
System.out.println(Arrays.toString(s1));
System.out.println(Arrays.toString(s2));
}
// 抛出异常`java.lang.ArrayIndexOutOfBoundsException`
private static void test07() {
int[] s1 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] s2 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
System.arraycopy(s1, 8, s2, 5, 3);
System.out.println(Arrays.toString(s1));
System.out.println(Arrays.toString(s2));
}
// 抛出异常`java.lang.ArrayIndexOutOfBoundsException`
从上面的测试可以了解到:
System.arraycopy()
就是将源数组的元素复制到目标数组中,- 如果源数组和目标数组是同一个数组,就可以实现数组内元素的移动,
- 源数组和目标数组的下标越界,都会抛出
ArrayIndexOutOfBoundsException
。
同时在ArrayList
进行数组操作的时候都会进行安全检查,包括下标检查和容量检查
// 容量检查
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table ? 0
// larger than default for default empty table. It's already
// supposed to be at default size. : DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
四、迭代方式
1. 随机访问
由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
for (int i=0, len = list.size(); i < len; i++) {
String s = list.get(i);
}
2. 迭代器遍历
这其实就是迭代器模式
Iterator iter = list.iterator();
while (iter.hasNext()) {
String s = (String)iter.next();
}
3. 增强for循环遍历
这其实是一个语法糖
for (String s : list) {
...
}
4. 增强for循环遍历的实现
- 对于
ArrayList
public void test_List() {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String s : list) {
System.out.println(s);
}
}
使用javap -v 反编译
Code:
stack=2, locals=4, args_size=1
...
27: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_2
33: aload_2
34: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
...
这里可以很清楚的看到,其实是通过Iterator
迭代器实现的
- 对于
Array
public void test_array() {
String[] ss = {"a", "b"};
for (String s : ss) {
System.out.println(s);
}
}
使用javap -v 反编译
Code:
stack=4, locals=6, args_size=1
0: iconst_2
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String a
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String b
13: aastore
14: astore_1
15: aload_1
16: astore_2
17: aload_2
18: arraylength
19: istore_3
20: iconst_0
21: istore 4 // 将一个数值从操作数栈存储到局部变量表
23: iload 4 // 将局部变量加载到操作栈
25: iload_3
26: if_icmpge 49
29: aload_2
30: iload 4
32: aaload
33: astore 5
35: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
38: aload 5
40: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: iinc 4, 1
46: goto 23
49: return
这里只能到导致看到是在做一些存取操作,但也不是很清楚,所以可以直接反编译成java
代码
public void test_array() {
String[] ss = new String[]{"a", "b"};
String[] var2 = ss;
int var3 = ss.length;
for(int var4 = 0; var4 < var3; ++var4) {
String s = var2[var4];
System.out.println(s);
}
}
现在就能很清楚的看到其实是通过随机存储(下标访问)的方式实现的;
五、fail-fast机制
fail-fast
是说当并发的对容器内容进行操作时,快速的抛出ConcurrentModificationException
;但是这种快速失败操作无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException
异常。所以我们程序的正确性不能完全依赖这个异常,只应用于bug检测。
protected transient int modCount = 0;
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
在ArrayList
的Iterator
和SubList
中,每当进行内存操作时,都会先使用checkForComodification
来检测内容是否已修改。
总结
ArrayList
整体来看就是一个更加安全和方便的数组,但是他的插入和删除操作也实在是蛋疼,对于这一点其实可以通过树或者跳表来解决。
JDK源码分析(3)之 ArrayList 相关的更多相关文章
- 【JDK】JDK源码分析-ArrayList
概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...
- 【JDK】JDK源码分析-Vector
概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...
- 【JDK】JDK源码分析-List, Iterator, ListIterator
List 是最常用的容器之一.之前提到过,分析源码时,优先分析接口的源码,因此这里先从 List 接口分析.List 方法列表如下: 由于上文「JDK源码分析-Collection」已对 Collec ...
- 【JDK】JDK源码分析-CountDownLatch
概述 CountDownLatch 是并发包中的一个工具类,它的典型应用场景为:一个线程等待几个线程执行,待这几个线程结束后,该线程再继续执行. 简单起见,可以把它理解为一个倒数的计数器:初始值为线程 ...
- JDK源码分析(三)—— LinkedList
参考文档 JDK源码分析(4)之 LinkedList 相关
- JDK源码分析(一)—— String
dir 参考文档 JDK源码分析(1)之 String 相关
- JDK源码分析(2)LinkedList
JDK版本 LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作. LinkedList 实现 ...
- 【JDK】JDK源码分析-HashMap(1)
概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...
- 【JDK】JDK源码分析-TreeMap(2)
前文「JDK源码分析-TreeMap(1)」分析了 TreeMap 的一些方法,本文分析其中的增删方法.这也是红黑树插入和删除节点的操作,由于相对复杂,因此单独进行分析. 插入操作 该操作其实就是红黑 ...
- 【JDK】JDK源码分析-AbstractQueuedSynchronizer(2)
概述 前文「JDK源码分析-AbstractQueuedSynchronizer(1)」初步分析了 AQS,其中提到了 Node 节点的「独占模式」和「共享模式」,其实 AQS 也主要是围绕对这两种模 ...
随机推荐
- 超时导致的Galera节点加入集群失败
需求:为galera集群添加新的节点. 初始化新的节点,加入的时候一直报错,加入失败,报错日志如下 WSREP_SST: [ERROR] Removing /var/lib/mysql//.sst/x ...
- W3C的标准到底是啥?
1.图片的alt="" 属性必须每张图片都加上,而且对齐属性用CSS来定义.不加不能通过XHTML 1.0的验证. 2.每个文档必须加上DTD声明. a) !DOCTYPE htm ...
- C++ otlv4 连接 sql server 数据库小记
otlv4介绍: http://otl.sourceforge.net/ 测试代码 // testotlv4.cpp : 定义控制台应用程序的入口点. // #include "stdafx ...
- Win 10 Revit 2019 安装过程,亲自踩的一遍坑,有你想要的细节
首先就是安装吖,不管是管理员权限还是普通权限,都是以下这个问题,跟权限没关系 failed to load .....revitcontentpackui.dll (126) 尝试了网上能查到的各种方 ...
- web项目部署到服务器中浏览器中显示乱码
项目部署之后浏览器打开查看时页面乱码 这里可能需要修改一下tomcat配置文件,首先找到Tomcat的安装路径下的conf/server.xml文件,找到之后可以CTRL+F搜索如下的内容: < ...
- git教程——工作流程
Git 工作流程 本章节我们将为大家介绍 Git 的工作流程. 一般工作流程如下: 克隆 Git 资源作为工作目录. 在克隆的资源上添加或修改文件. 如果其他人修改了,你可以更新资源. 在提交前查看修 ...
- 33 ArcToolBox学习系列之数据管理工具箱——投影与变换(Projections and Transformations)未完待续……
工具箱位置 打开ArcToolBox,找到工具集Projections and Transformations,位置如下:ArcToolbox--Data Management Tools--Proj ...
- window server 2008 安装Oracle10g
oracle安装都大同小异. 开始安装步骤 输入完之后点击下一步 这时候稍等一会儿. 这时候也要稍等一会儿. 直接安装. 设置口令管理,设置SCOTT的密码为tiger就好了. 这时候稍等一会儿. o ...
- tensorflow-变量
#计数器 import tensorflow as tf state = tf.Variable(0,name='counter') #设定变量print(state.name) #打印变量one = ...
- MS-UAP发布的UWP的个人隐私策略
我们十分重视您的隐私.本隐私声明解释了我们从您那里收集的个人数据内容以及我们将如何使用这些数据. 我们不收集任何与个人信息相关的数据,只收集与本UWP运行相关的数据,如: 产品使用数据:如每个页面的使 ...