ArrayList的实现细节(基于JDK1.8)

ArrayList是我们经常用到的一个类,下面总结一下它内部的实现细节和使用时要注意的地方。

基本概念

ArrayList在数据结构的层面上讲,是一个用数组实现的list,从应用层面上讲,就是一个容量会自己改变的数组,具有一系列方便的add、set、get、remove等方法,线程不安全。先上张类图吧。

ArrayList的容量

ArrayList有两个数据域与之相关。

1     transient Object[] elementData; // non-private to simplify nested class access
2 
3     private int size;

很明显,size表示ArrayList中包含的元素数量,也就是size()方法的返回值,而elementData.length则是ArrayList的容量,表示在不扩容的情况下能存储多少个元素。By the way,JDK1.8的ArrayList的初始容量是0,之前的版本貌似10。
ArrayList还有一些关于扩大容量和缩小容量的方法

 1     /**
 2      * 当ArrayList中有空闲的空间时,缩减ArrayList的容量。应用程序可以使用这个方法最小化ArrayList实例
 3      */
 4     public void trimToSize()
 5 
 6     /**
 7      * public修饰,供应用程序调用的扩容方法,内部调用ensureExplicitCapacity()方法
 8      */
 9     public void ensureCapacity(int minCapacity)
10 
11     /**
12      * private修饰,供ArrayList内部使用的扩容方法,内部同样是调用ensureExplicitCapacity()方法
13      */
14     private void ensureCapacityInternal(int minCapacity)
15 
16     /**
17      * 内部调用grow()方法
18      */
19     private void ensureExplicitCapacity(int minCapacity)
20 
21     /**
22      * grow()方法内部会做一个判断,如果ArrayList扩大1.5倍还不够的话,才会增加到minCapacity
23      * 这是为了防止扩容太小而导致多次扩容多次改变数组大小,从而影响性能。
24      * 比如说,我有一个装满了的ArrayList,现在我往其中加入10个元素,自然是要扩容的,
25      * 那么我是一次性扩容增加10个容量,还是每次add前扩容增加一个容量呢,答案可想而知。
26      */
27     private void grow(int minCapacity)
28 
29     /**
30      * 对ArrayList扩容的一个限制,扩得太大会抛出OutOfMemoryError
31      */
32     private static int hugeCapacity(int minCapacity)

虽说的容量会随着数据量的增大而增大,使用时不用费心于容量的维护,不过在可以预估数据量的情况下,务必使用public ArrayList(int initialCapacity)来指定初始容量,这样的话,一来减少扩容方法的调用避免数组频繁更改,二来在一定程度上减少了内存的消耗(比如我就存5000个元素,当数组达到4000时扩容扩大1.5倍变成6000,白白耗费了1000个单位的内存)。经过测试,这是可以大大提高运行效率的。

Clone

ArrayList的clone()方法是浅复制,在这里直接上段demo。

 1     public class Main {
 2         public static void main(String[] args) {
 3             User u1 = new User();
 4             u1.setUsername("qwe");
 5             u1.setPassword("qwePASSWORD");
 6             User u2 = new User();
 7             u2.setUsername("asd");
 8             u2.setPassword("asdPassword");
 9             ArrayList<User> list1 = new ArrayList<>();
10             list1.add(u1);
11             list1.add(u2);
12             ArrayList<User> list2 = (ArrayList<User>) list1.clone();
13             list2.get(0).setUsername("zxc"); //修改u1的username
14             list2.get(0).setPassword("zxcPassword"); ////修改u1的password
15             System.out.println(list1); //[User [username=zxc, password=zxcPassword], User                                             //[username=asd, password=asdPassword]]
16         }
17         /**
18          * 实现深复制
19          */
20         private static List<User> deepClone(List<User> from) throws CloneNotSupportedException {
21             List<User> list = new ArrayList<>();
22             for(User item : from) {
23                 list.add((User)item.clone());
24             }
25             return list;
26         }
27     }
28 
29     class User {
30         private String username;
31         private String password;
32         public User() {
33         }
34         public String getUsername() {
35             return username;
36         }
37         public void setUsername(String username) {
38             this.username = username;
39         }
40         public String getPassword() {
41             return password;
42         }
43         public void setPassword(String password) {
44             this.password = password;
45         }
46         @Override
47         public String toString() {
48             return "User [username=" + username + ", password=" + password + "]";
49         }
50     }

有输出可知,list2中的u1就是list1中的u1,二者的引用指向了同一个User对象,具体见示意图。所以要想实现ArrayList的深复制得根据场景自己写。

public Object[] toArray()public T[] toArray(T[] a)

 1     /**
 2      * 获得一个Object数组,这个方法会分配一个新数组(并不是单纯的return elementData;),所以调用者可以安全的修改数组而不影响ArrayList
 3      */
 4     public Object[] toArray()
 5 
 6     /**
 7      * 获得一个泛型数组
 8      */
 9     public <T> T[] toArray(T[] a) {
10         if (a.length < size) //数组a长度不足,则重新new一个数组
11             // Make a new array of a's runtime type, but my contents:
12             return (T[]) Arrays.copyOf(elementData, size, a.getClass());
13         System.arraycopy(elementData, 0, a, 0, size); //数组a长度足够,就将元素复制到a数组中,而后返回a
14         if (a.length > size)
15             a[size] = null;
16         return a;
17     }

This method acts as bridge between array-based and collection-based APIs.这是文档注释中的一句话,大意是这个方法是数组和集合之间的桥梁。通过函数签名,我们可以得知toArray()返回一个Object数组,toArray(T[] a)返回一个泛型数组。我们往往使用的是toArray(T[] a),常见的使用方式如下

1     List<Integer> list = new ArrayList<>();
2     Collections.addAll(list,1,2,3,4,5,6);
3     // 方式1 //
4     list.toArray(new Integer[0]); //涉及到反射,效率较低
5     // 方式2 //
6     list.toArray(new Integer[list.size()])

构造函数:public ArrayList(Collection c)

 1     public ArrayList(Collection<? extends E> c) {
 2         elementData = c.toArray();
 3         if ((size = elementData.length) != 0) {
 4             // c.toArray might (incorrectly) not return Object[] (see 6260652)
 5             if (elementData.getClass() != Object[].class)
 6                 elementData = Arrays.copyOf(elementData, size, Object[].class);
 7         } else {
 8             // replace with empty array.
 9             this.elementData = EMPTY_ELEMENTDATA;
10         }
11     }

利用这个构造方法,我们可以方便的使用其他容器来构造一个ArrayList。这里有一个要点,通过源码我们得知,当elementData不是Object数组时,它会使用Arrays.copyOf()方法构造一个Object数组替换elementData,为什么要这么做呢,Object[] objArr = new String[5];之类的代码完全不会报错啊。我们先看一段代码,理解Java数组的一个特性,Java数组要求其存储的元素必须是new数组时的实际类型的实例。

1     Object[] objArr = new String[5];
2     objArr[0] = "qwe";
3     objArr[1] = new Object(); //java.lang.ArrayStoreException
4     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的实现更加严格,在单线程环境下作死的话也会抛出异常。

1     List<Integer> list = new ArrayList<Integer>();
2     Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7);
3     Iterator<Integer> iterator = list.iterator();
4     list.add(8); //修改了ArrayList
5     while(iterator.hasNext()) {
6         System.out.println(iterator.next()); //java.util.ConcurrentModificationException
7     }

下面再简单讲几句ArrayList实现快速失败的机制。ArrayList的快速失败是围绕着迭代器的,所以定位到迭代器的源码。获得一个迭代器后, expectedModCount值就确定了,可是modCount可能会改变(trimToSize()、ensureExplicitCapacity()、remove()、clear()等等都会修改modCount)。往后使用迭代器的过程中,一旦expectedModCount不等于modCount,就认为迭代的结果有问题,不管三七二十一就抛出ConcurrentModificationException。

 1     private class Itr implements Iterator<E> {
 2         /**
 3          * 每构造一个迭代器都会记录当前的modCount,modCount之后有可能会改变
 4          */
 5         int expectedModCount = modCount;
 6         /**
 7          * 当modCount不等于expectedModCount就抛出ConcurrentModificationException
 8          */
 9         final void checkForComodification() {
10             if (modCount != expectedModCount)
11                 throw new ConcurrentModificationException();
12         }
13     }

务必理解文档注释中的一段话。

1 he iterators returned by this class's iterator and listIterator methods are fail-fast: 
2 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. 
3 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. 
4 
5 快速失败是指:迭代器被创建后,list发生了结构型的变化(除了使用迭代器自己的add或者remove操作),迭代器使用时会抛出ConcurrentModificationException。
6 该类的iterator和listIterator都是快速失败的。
7 因此,面对并发修改,迭代器将快速的抛出异常终止迭代,而不是冒着风险在非确定的未来进行非确定性行为。

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循环遍历效率更高。

1     ArrayList<Integer> list = new ArrayList<>();
2     Collections.addAll(list,1,2,3,4,5,6,7);
3     int len = list.size(); //避免重复调用list.size()方法
4     for(int i=0;i<len;i++) {
5         System.out.print(list.get(i)); //随机访问
6     }

 

RandomAccess接口

RandomAccess是一个标记接口,用于标记当前类是可以随机访问的,有什么用?我们先看看JDK中一个典型的应用场景。

 1     /**
 2      * Collections.fill()
 3      */
 4     public static <T> void fill(List<? super T> list, T obj) {
 5         int size = list.size();
 6         if (size < FILL_THRESHOLD || list instanceof RandomAccess) {
 7             for (int i=0; i<size; i++)
 8                 list.set(i, obj);
 9         } else {
10             ListIterator<? super T> itr = list.listIterator();
11             for (int i=0; i<size; i++) {
12                 itr.next();
13                 itr.set(obj);
14             }
15         }
16     }

上面这段代码,大概的业务逻辑是指当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()来编写代码了!

 1     public void add(int index, E element) {
 2         rangeCheckForAdd(index); //检查index有没有越界
 3         ensureCapacityInternal(size + 1);  // Increments modCount!!
 4         System.arraycopy(elementData, index, elementData, index + 1,
 5                          size - index); //将elementData位于index之后的元素全部向后移一位
 6         elementData[index] = element;
 7         size++;
 8     }
 9 
10     public E remove(int index) {
11         rangeCheck(index);//检查index有没有越界
12         modCount++;
13         E oldValue = elementData(index);
14         int numMoved = size - index - 1;
15         if (numMoved > 0)
16             System.arraycopy(elementData, index+1, elementData, index,
17                              numMoved);//将elementData位于index+1之后的元素全部向前移一位
18         elementData[--size] = null; // clear to let GC do its work
19         return oldValue;
20     }

总结

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

posted @ 2017-08-02 11:20  付大石  阅读(1627)  评论(0编辑  收藏  举报