ArrayList的实现细节(基于JDK1.8)
ArrayList是我们经常用到的一个类,下面总结一下它内部的实现细节和使用时要注意的地方。
基本概念
ArrayList在数据结构的层面上讲,是一个用数组实现的list,从应用层面上讲,就是一个容量会自己改变的数组,具有一系列方便的add、set、get、remove等方法,线程不安全。先上张类图吧。
ArrayList的容量
ArrayList有两个数据域与之相关。
transient Object[] elementData; // non-private to simplify nested class access private int size;
很明显,size表示ArrayList中包含的元素数量,也就是size()方法的返回值,而elementData.length则是ArrayList的容量,表示在不扩容的情况下能存储多少个元素。By the way,JDK1.8的ArrayList的初始容量是0,之前的版本貌似10。
ArrayList还有一些关于扩大容量和缩小容量的方法
/**
* 当ArrayList中有空闲的空间时,缩减ArrayList的容量。应用程序可以使用这个方法最小化ArrayList实例
*/
public void trimToSize() /**
* public修饰,供应用程序调用的扩容方法,内部调用ensureExplicitCapacity()方法
*/
public void ensureCapacity(int minCapacity) /**
* private修饰,供ArrayList内部使用的扩容方法,内部同样是调用ensureExplicitCapacity()方法
*/
private void ensureCapacityInternal(int minCapacity) /**
* 内部调用grow()方法
*/
private void ensureExplicitCapacity(int minCapacity) /**
* grow()方法内部会做一个判断,如果ArrayList扩大1.5倍还不够的话,才会增加到minCapacity
* 这是为了防止扩容太小而导致多次扩容多次改变数组大小,从而影响性能。
* 比如说,我有一个装满了的ArrayList,现在我往其中加入10个元素,自然是要扩容的,
* 那么我是一次性扩容增加10个容量,还是每次add前扩容增加一个容量呢,答案可想而知。
*/
private void grow(int minCapacity) /**
* 对ArrayList扩容的一个限制,扩得太大会抛出OutOfMemoryError
*/
private static int hugeCapacity(int minCapacity)
虽说的容量会随着数据量的增大而增大,使用时不用费心于容量的维护,不过在可以预估数据量的情况下,务必使用public ArrayList(int initialCapacity)来指定初始容量,这样的话,一来减少扩容方法的调用避免数组频繁更改,二来在一定程度上减少了内存的消耗(比如我就存5000个元素,当数组达到4000时扩容扩大1.5倍变成6000,白白耗费了1000个单位的内存)。经过测试,这是可以大大提高运行效率的。
Clone
ArrayList的clone()方法是浅复制,在这里直接上段demo。
public class Main {
public static void main(String[] args) {
User u1 = new User();
u1.setUsername("qwe");
u1.setPassword("qwePASSWORD");
User u2 = new User();
u2.setUsername("asd");
u2.setPassword("asdPassword");
ArrayList<User> list1 = new ArrayList<>();
list1.add(u1);
list1.add(u2);
ArrayList<User> list2 = (ArrayList<User>) list1.clone();
list2.get(0).setUsername("zxc"); //修改u1的username
list2.get(0).setPassword("zxcPassword"); ////修改u1的password
System.out.println(list1); //[User [username=zxc, password=zxcPassword], User //[username=asd, password=asdPassword]]
}
/**
* 实现深复制
*/
private static List<User> deepClone(List<User> from) throws CloneNotSupportedException {
List<User> list = new ArrayList<>();
for(User item : from) {
list.add((User)item.clone());
}
return list;
}
} class User {
private String username;
private String password;
public User() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + "]";
}
}
有输出可知,list2中的u1就是list1中的u1,二者的引用指向了同一个User对象,具体见示意图。所以要想实现ArrayList的深复制得根据场景自己写。
public Object[] toArray()和public T[] toArray(T[] a)
/**
* 获得一个Object数组,这个方法会分配一个新数组(并不是单纯的return elementData;),所以调用者可以安全的修改数组而不影响ArrayList
*/
public Object[] toArray() /**
* 获得一个泛型数组
*/
public <T> T[] toArray(T[] a) {
if (a.length < size) //数组a长度不足,则重新new一个数组
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size); //数组a长度足够,就将元素复制到a数组中,而后返回a
if (a.length > size)
a[size] = null;
return a;
}
This method acts as bridge between array-based and collection-based APIs.这是文档注释中的一句话,大意是这个方法是数组和集合之间的桥梁。通过函数签名,我们可以得知toArray()返回一个Object数组,toArray(T[] a)返回一个泛型数组。我们往往使用的是toArray(T[] a),常见的使用方式如下
List<Integer> list = new ArrayList<>();
Collections.addAll(list,1,2,3,4,5,6);
// 方式1 //
list.toArray(new Integer[0]); //涉及到反射,效率较低
// 方式2 //
list.toArray(new Integer[list.size()])
构造函数:public ArrayList(Collection 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;
}
}
利用这个构造方法,我们可以方便的使用其他容器来构造一个ArrayList。这里有一个要点,通过源码我们得知,当elementData不是Object数组时,它会使用Arrays.copyOf()方法构造一个Object数组替换elementData,为什么要这么做呢,Object[] objArr = new String[5];之类的代码完全不会报错啊。我们先看一段代码,理解Java数组的一个特性,Java数组要求其存储的元素必须是new数组时的实际类型的实例。
Object[] objArr = new String[5];
objArr[0] = "qwe";
objArr[1] = new Object(); //java.lang.ArrayStoreException
System.out.println(Arrays.toString(objArr));
数组objArr的实际类型是String数组,所以它只能存储Stirng类型的对象实例(String没有子类),不然就抛出异常。
理解了ArrayStoreException,我们再回到ArrayList。假设在使用上面那个构造函数时,不转换成Object数组类型,当我们使用toArray()方法时就会出问题了,正如注释所说:c.toArray
might (incorrectly) not return
Object[]。使用toArray()方法获得一个Object数组,直观意思就是可以往里面加任何类型的实例啊,但是如果不在上面那个构造函数中特殊处理,是会抛java.lang.ArrayStoreException。这就是为什么ArrayList要对非Object数组特殊处理:为了toArray()返回的Object数组能够正常使用。
List list = new ArrayList(new StringCollection()); //假设StringCollection集合内部是一个String数组
Object[] arr = list.toArray(); // 由于构造函数转换了数组类型,所以这个arr数组可以正常使用,真是nice啊
System.out.println(arr.getClass());// class [Ljava.lang.Object;返回的是Object数组
arr[0] = "";
arr[0] = 123;
arr[0] = new Object();
fail-fast:快速失败
fail-fast是指在多线程环境下,比如一个线程在读(这里仅考虑迭代器迭代),一个线程在写的情况下容易出现匪夷所思的bug,为了更好的调试,采用了快速失败机制,一旦发现异步修改,马上抛异常而不是继续迭代下去。当然,ArrayListd的实现更加严格,在单线程环境下作死的话也会抛出异常。
List<Integer> list = new ArrayList<Integer>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7);
Iterator<Integer> iterator = list.iterator();
list.add(8); //修改了ArrayList
while(iterator.hasNext()) {
System.out.println(iterator.next()); //java.util.ConcurrentModificationException
}
下面再简单讲几句ArrayList实现快速失败的机制。ArrayList的快速失败是围绕着迭代器的,所以定位到迭代器的源码。获得一个迭代器后, expectedModCount值就确定了,可是modCount可能会改变(trimToSize()、ensureExplicitCapacity()、remove()、clear()等等都会修改modCount)。往后使用迭代器的过程中,一旦expectedModCount不等于modCount,就认为迭代的结果有问题,不管三七二十一就抛出ConcurrentModificationException。
private class Itr implements Iterator<E> {
/**
* 每构造一个迭代器都会记录当前的modCount,modCount之后有可能会改变
*/
int expectedModCount = modCount;
/**
* 当modCount不等于expectedModCount就抛出ConcurrentModificationException
*/
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
务必理解文档注释中的一段话。
he iterators returned by this class's iterator and listIterator methods are fail-fast:
if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException.
Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. 快速失败是指:迭代器被创建后,list发生了结构型的变化(除了使用迭代器自己的add或者remove操作),迭代器使用时会抛出ConcurrentModificationException。
该类的iterator和listIterator都是快速失败的。
因此,面对并发修改,迭代器将快速的抛出异常终止迭代,而不是冒着风险在非确定的未来进行非确定性行为。
ArrayList的序列化机制
通过UML图,我们知道ArrayList实现了Serializable接口,通过源码,我们又知道ArrayList的序列化机制、反序列化机制是自定义的。
/**
* 自定义序列化机制
*/
private void writeObject(java.io.ObjectOutputStream s) /**
* 自定义反序列化机制
*/
private void readObject(java.io.ObjectInputStream s)
那么为什么要自定义序列化、反序列化机制呢?是由于ArrayList实质上是一个动态数组,往往数组中会有空余的空间,如果采用默认的序列化机制,那些空余的空间会作为null写入本地文件或者在网络中传输,耗费了不必要的资源。所以,ArrayList使用自定义序列化机制,仅写入索引为【0,size)的有效元素以节省资源。
ArrayList的遍历
ArrayList的遍历方式有三种:foreach语法糖、普通for循环,迭代器。其中foreach相当于使用迭代器遍历,而是用迭代器时会有个迭代器对象的开销,所以一般情况下普通的for循环遍历效率更高。
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,1,2,3,4,5,6,7);
int len = list.size(); //避免重复调用list.size()方法
for(int i=0;i<len;i++) {
System.out.print(list.get(i)); //随机访问
}
RandomAccess接口
RandomAccess是一个标记接口,用于标记当前类是可以随机访问的,有什么用?我们先看看JDK中一个典型的应用场景。
/**
* Collections.fill()
*/
public static <T> void fill(List<? super T> list, T obj) {
int size = list.size();
if (size < FILL_THRESHOLD || list instanceof RandomAccess) {
for (int i=0; i<size; i++)
list.set(i, obj);
} else {
ListIterator<? super T> itr = list.listIterator();
for (int i=0; i<size; i++) {
itr.next();
itr.set(obj);
}
}
}
上面这段代码,大概的业务逻辑是指当list是RandomAccess的实例时,便用普通的for循环遍历,如果不是RandomAccess实例时,则用迭代器遍历。
前面一点已经讲了,对于ArrayList,普通的for循环遍历效率比用迭代器遍历效率高。现在拓展这一点:当一个类标记了RandomAccess接口,那么表明该类使用for循环遍历效率更高,如果没用RandomAccess标记,则使用迭代器遍历效率更高。平时我们可以模仿Collections.fill(),使用这个特性写出更美好的代码。
另外,如果使用普通的for循环遍历非RandomAccess的实例,效率是很低的,比如LinkedList(实质是一个双向链表),每次get一个元素都要遍历半个链表,所以要格外注意。
System.arraycopy()方法
记得刚学数据结构时,删除一个元素,添加一个元素是这么写的。
/**
* 在第索引{@param i}处插入元素{@param item}
*/
@Override
public void add(int i, T item) {
// 参数校验 //
if (i < 0 || i > size) {
throw new IllegalArgumentException(String.format("i=%d,无效索引值", i));
}
// 插入元素 //
for (int p = size; p > i; p--) { // 移动数组
arr[p] = arr[p - 1];
}
arr[i] = item;
size++;
} /**
* 删除索引{@param i}处的元素
*/
@Override
public T remove(int i) {
// 参数校验 //
if (i < 0 || i >= size) {
throw new IllegalArgumentException(String.format("i=%d,无效索引值", i));
}
// 移除节点 //
T item = arr[i];
for (int p = i; p < size - 1; p++) { // 移动数组
arr[p] = arr[p + 1];
}
arr[--size] = null;
return item;
}
不论添加、删除,因为移动数组,所以得用for循环来移动,而且循环的边界条件很难掌握很容易写错,而ArrayList使用了System.arraycopy()来简化的这一切。掌握了这个,平时我们也可以使用System.arraycopy()来编写代码了!
public void add(int index, E element) {
rangeCheckForAdd(index); //检查index有没有越界
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //将elementData位于index之后的元素全部向后移一位
elementData[index] = element;
size++;
} public E remove(int index) {
rangeCheck(index);//检查index有没有越界
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//将elementData位于index+1之后的元素全部向前移一位
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
总结
ArrayList是一个线程不安全的动态数组,使用ensureCapacity()扩容,trimToSize缩减容量。
toArray()的使用
System.arraycopy()的使用
引用
1.http://www.cnblogs.com/skywang12345/p/3308556.html
2.http://blog.csdn.net/jzhf2012/article/details/8540410
3.http://blog.csdn.net/ljcITworld/article/details/52041836
4.http://www.cnblogs.com/dolphin0520/p/3933551.html
5.http://www.cnblogs.com/ITtangtang/p/3948555.html
6.http://www.cnblogs.com/java-zhao/p/5102342.html
7.http://www.tuicool.com/articles/uIBB3q
8.http://blog.csdn.net/gulu_gulu_jp/article/details/51457492
9.http://blog.csdn.net/chenssy/article/details/38373833
10.https://www.zhihu.com/question/19882918
11.http://www.cnblogs.com/vinozly/p/5171227.html
ArrayList的实现细节(基于JDK1.8)的更多相关文章
- ArrayList 源码分析 基于jdk1.8:
1:数据结构: transient Object[] elementData; //说明内部维护的数据结构是一个Object[] 数组 成员属性: private static final int ...
- ArrayList源码解读(jdk1.8)
概要 上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解Arra ...
- Java集合基于JDK1.8的ArrayList源码分析
本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组.数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集 ...
- 基于JDK1.8的ArrayList剖析
前言 本文是基于JDK1.8的ArrayList进行分析的.本文大概从以下几个方面来分析ArrayList这个数据结构 构造方法 add方法 扩容 remove方法 (一)构造方法 /** * Con ...
- Java集合(四)--基于JDK1.8的ArrayList源码解读
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess ...
- Java集合类源码解析:HashMap (基于JDK1.8)
目录 前言 HashMap的数据结构 深入源码 两个参数 成员变量 四个构造方法 插入数据的方法:put() 哈希函数:hash() 动态扩容:resize() 节点树化.红黑树的拆分 节点树化 红黑 ...
- 基于JDK1.8的LinkedList源码学习笔记
LinkedList作为一种常用的List,是除了ArrayList之外最有用的List.其同样实现了List接口,但是除此之外它同样实现了Deque接口,而Deque是一个双端队列接口,其继承自Qu ...
- Java集合基于JDK1.8的LinkedList源码分析
上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于 ...
- 基于JDK1.8的HashMap分析
HashMap的强大功能,相信大家都了解一二.之前看过HashMap的源代码,都是基于JDK1.6的,并且知其然不知其所以然,现在趁着寒假有时间,温故而知新.文章大概有以下几个方面: HashMap的 ...
- 基于JDK1.8版本的hashmap源码笔记(二)
这一篇是接着上一篇写的, 上一篇的地址是:基于JDK1.8版本的hashmap源码分析(一) /** * 返回boolean类型的值,当集合中包含key的键值,就返回true,否则就返 ...
随机推荐
- 在centos6.7中lnmp环境下安装swoole插件和pthreads插件
1.首先在安装lnmp集成包之前,解压lnmp1.3-full.tar.gz,进入到lnmp1.3-full/include/目录下; 2.输入 vi php.sh;编辑php.sh文档.博主安的是p ...
- ant使用
摘录于他人精华,原文出处http://www.blogjava.net/hoojo/archive/2013/06/14/400550.html 1.project 节点元素 project 元素是 ...
- 我的学习之路_第二十章_JDBC
JDBC 使用JDBC技术,通过mysql提供的驱动程序,操作数据库 ● 1. 注册驱动 告知jvm 使用的是什么驱动程序(mysql,oracle) 使用API中的类 DriverManager中的 ...
- mysql 左连接 右连接 内链接
一般所说的左连接,右连接是指左外连接,右外连接.做个简单的测试你看吧.先说左外连接和右外连接:[TEST1@orcl#16-12月-11] SQL>select * from t1;ID NAM ...
- 【Android Developers Training】 13. 支持不同平台版本
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- if和for的几个经典题目
1.有一对幼兔,幼兔1个月后长成小兔,小兔1个月后长成成兔并生下一对幼兔,问几年后有多少对兔子,幼兔.小兔.成兔对数分别是多少. 幼兔 1 小兔 0 成兔 0幼兔 0 小兔 1 成兔 0 幼兔 1 小 ...
- SequoiaDB x Spark 新主流架构引领企业级应用
6月,汇集当今大数据界精英的Spark Summit 2017盛大召开,Spark作为当今最炙手可热的大数据技术框架,向全世界展示了最新的技术成果.生态体系及未来发展规划. 巨杉作为业内领先的分布式数 ...
- Tagged Pointer
前言 在2013年9月,苹果推出了iPhone5s,与此同时,iPhone5s配备了首个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念.对于6 ...
- hack在微信等webview中无法修改document.title的情况
var $body = $('body'); document.title = '确认车牌'; // hack在微信等webview中无法修改document.title的情况 var $iframe ...
- angular4.0 父子组建之间的相互通信
父组建---->子组建 传递信息 首先先通过angular脚手架生成两个基本组件,有一个好处是 会自动关联到跟模版,节约时间,而且还是偷懒 ng generate component compo ...