• 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

  • 相关阅读:
    JavaScript的object和Array引用类型
    JavaScript中JSON的序列化与解析
    JavaScript获取url后面的参数
    JavaScript事件处理程序
    JavaScript手机端页面滑动到底部加载信息(移动端ajax分页)
    666
    jquery的键盘事件
    如何判断是不是微信登录浏览器
    写的挺好 placeholder 的模拟用法
    下雪了还是下冰雹了
  • 原文地址:https://www.cnblogs.com/fudashi/p/7272930.html
Copyright © 2020-2023  润新知