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)的更多相关文章

  1. ArrayList 源码分析 基于jdk1.8:

    1:数据结构: transient Object[] elementData;  //说明内部维护的数据结构是一个Object[] 数组 成员属性: private static final int ...

  2. ArrayList源码解读(jdk1.8)

    概要 上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解Arra ...

  3. Java集合基于JDK1.8的ArrayList源码分析

    本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组.数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集 ...

  4. 基于JDK1.8的ArrayList剖析

    前言 本文是基于JDK1.8的ArrayList进行分析的.本文大概从以下几个方面来分析ArrayList这个数据结构 构造方法 add方法 扩容 remove方法 (一)构造方法 /** * Con ...

  5. Java集合(四)--基于JDK1.8的ArrayList源码解读

    public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess ...

  6. Java集合类源码解析:HashMap (基于JDK1.8)

    目录 前言 HashMap的数据结构 深入源码 两个参数 成员变量 四个构造方法 插入数据的方法:put() 哈希函数:hash() 动态扩容:resize() 节点树化.红黑树的拆分 节点树化 红黑 ...

  7. 基于JDK1.8的LinkedList源码学习笔记

    LinkedList作为一种常用的List,是除了ArrayList之外最有用的List.其同样实现了List接口,但是除此之外它同样实现了Deque接口,而Deque是一个双端队列接口,其继承自Qu ...

  8. Java集合基于JDK1.8的LinkedList源码分析

    上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于 ...

  9. 基于JDK1.8的HashMap分析

    HashMap的强大功能,相信大家都了解一二.之前看过HashMap的源代码,都是基于JDK1.6的,并且知其然不知其所以然,现在趁着寒假有时间,温故而知新.文章大概有以下几个方面: HashMap的 ...

  10. 基于JDK1.8版本的hashmap源码笔记(二)

    这一篇是接着上一篇写的, 上一篇的地址是:基于JDK1.8版本的hashmap源码分析(一)     /**     * 返回boolean类型的值,当集合中包含key的键值,就返回true,否则就返 ...

随机推荐

  1. .net 4.0 中的特性总结(三):垃圾回收

    1.内存基础知识 每个进程都有其自己单独的虚拟地址空间. 同一台计算机上的所有进程共享相同的物理内存,如果有页文件,则也共享页文件. 默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式 ...

  2. 这是您一直期待的所有iOS 11功能的屏幕截图

    Tips 原文作者:Chris Mills 原文地址:Here's all the iOS 11 screenshots you've been waiting for 除非你已经深陷VR其中,否则现 ...

  3. phpExcel读取excel文件数据

    require_once $_SERVER['DOCUMENT_ROOT'].'/Classes/PHPExcel.php';require_once $_SERVER['DOCUMENT_ROOT' ...

  4. 禁用自动映射的 Exchange 邮箱

    客户最近询问他们无法从用户的Outlook配置文件中删除邮箱.在这种情况下,它们是共享邮箱,并出现在Outlook的左窗格中.但原因和解决方法同样适用于用户邮箱.并且 无论用户邮箱在本地 Exchan ...

  5. 图论基础之Dijkstra算法的初探

         图论,顾名思义就是有图有论.        图:由点"Vertex"和边"Edge "组成,且图分为有向图和无向图(本文讨论有向图),之前做毕业设计的 ...

  6. 我的学习之路_第二十七章_jQuery

    jQueryjs类库 把常用对象或者方法封装起来,让我们写代码效率更高 1.jQuery 2.extjs [jQuery入门] jQuery的引入: 通过script标签的src属性引入 入门: 获取 ...

  7. springmvc的POST 请求转为 DELETE 或 put 请求配置HiddenHttpMethodFilter

    1.web.xml里配置 <!-- 配置 org.springframework.web.filter.HiddenHttpMethodFilter: 可以把 POST 请求转为 DELETE ...

  8. 新闻信息的javaBean

    //新闻信息的javaBeanpublic class News { //新闻属性 private int id; //id private int categoryId ;//新闻类别id priv ...

  9. Scrapy爬虫实例教程(二)---数据存入MySQL

    书接上回 实例教程(一) 本文将详细描述使用scrapy爬去左岸读书所有文章并存入本地MySql数据库中,文中所有操作都是建立在scrapy已经配置完毕,并且系统中已经安装了Mysql数据库(有权限操 ...

  10. Watson Conversation Service Implementation Methodology

    Watson Conversation Service Implementation Methodology In order to implement the WCS successfully. Y ...