zoukankan      html  css  js  c++  java
  • Java集合之ArrayList

    基本特性

    对于集合具体实现类来说,首先需要掌握的基本特性是:

    • 元素是否允许为null
    • 元素是否允许重复
    • 是否有序,指读取数据的顺序是否与存储数据的顺序一致
    • 是否线程安全

    对于ArrayList,如下表:

    基本特性 结论
    元素是否允许为null
    元素是否允许重复
    是否有序
    是否线程安全

    源码分析

    本文使用的是JDK 1.8.0_201的源码。

    成员变量

    ArrayList是一个底层以数组实现的集合,它最主要的成员变量是:

    成员变量 作用
    transient Object[] elementData; elementData作为底层数组
    private int size; 集合中元素的个数,不同与elementData数组的长度

    添加元素操作

    ArrayList是用数组实现的,在Java中数组的长度是不可变的,数组在初始化的时候就要指定大小,我们知道ArrayList初始化时可以不指定大小,那么ArrayList是如何实现动态数组扩容的呢?

    先看 add(E) 方法:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    

    ArrayList每次添加元素时,都首先调用ensureCapacityInternal(size + 1)方法,确保数组的容量。我们跟到ensureCapacityInternal(size + 1)方法中去:

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        // 这个值用于遍历时的快速失败,避免并发时导致的不可预料的错误
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    

    上面calculateCapacity(elementData, minCapacity)方法判断ArrayList初始化时是否指定了大小,如果没有指定大小返回默认大小10。接着,modCount++操作与扩容关系不大,它用于遍历时的快速失败,避免并发时导致的不可预料的错误。最后的条件判断才是真正进行数组扩容的地方,继续跟到grow(minCapacity)方法中去:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 将数组的大小扩大的原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    int newCapacity = oldCapacity + (oldCapacity >> 1),这行代码将数组的大小扩大的原来的1.5倍。而进行数组扩容的代码Arrays.copyOf(elementData, newCapacity),底层调用的是System.arraycopy()方法,这个方法是本地方法,效率比较高。我们自己在实现数组扩容时,也可以直接调用这个本地方法,提高程序性能。

    删除元素操作

    ArrayList支持两种删除方式:

    • 按照下标进行删除
    • 按照元素进行删除,ArrayList允许元素重复,这个操作只会删除第一个匹配的元素

    对于ArrayList来说,由于其底层是数组实现,那么数组在删除元素后,需要将该元素之后的每个元素都向前移动一个位置,因此效率是比较低的。

    两种删除方式的逻辑略有不同,但是底层的删除操作都是下面这段代码:

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
    

    插入元素操作

    ArrayList插入元素的操作也是使用的add()方法,只不过参数不同:

    public void add(int index, E element) {
        rangeCheckForAdd(index);
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    

    同样地,由于ArrayList是数组实现,因此插入元素将导致插入位置后的所有元素向后移动一位,因此效率不高。

    ArrayList的优缺点

    优点:

    • 由于底层用数组实现,因此通过下标进行的随机访问,比如get(int index)、set(int index, E e)等操作会比较快。
    • 由于底层用数组实现,每个元素存储占用的内存空间相对较小。

    缺点:

    • 由于底层用数组实现,在删除和插入元素时,需要移动元素,在元素较多情况下,效率会比较低。

    综上所述,ArrayList只适合在对数据进行存储和访问的情况下使用,不适用频繁修改数据的场景。

    ArrayList和Vector的区别

    两者之间最大的区别就在于,ArrayList非线程安全,而Vector是线程安全的。尽管Vector是线程安全的,不代表在多线程的情况下就应该使用Vector。事实上我们应该避免使用Vector,因为Vector是在每个独立的方法上进行同步,而不是对整个集合数据进行同步,在进行迭代的时候可能会抛出ConcurrentModificationException。

    除此之外,Vector即是“数组动态扩容”的实现又是同步操作的实现,违反了面向对象的“单一职责”设计原则。我们更应该使用装饰器模式的Collections.synchronizedList()。

    无论是Vector还是Collections.synchronizedList(),采用的都是同步的方式来实现线程的安全性。这种方式将会降低并发性,当线程竞争激烈时,会严重影响程序的性能。Java 5.0提供了多种并发容器类,其中CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步List。

    为什么ArrayList的elementData是用transient修饰的?

    ArrayList中的数组,是这么定义的:

    private transient Object[] elementData;
    

    ArrayList是可以序列化的,而elementData被transient关键字修饰后,将不会被序列化,那么为什么要这么做呢?

    因为ArrayList序列化时,elementData数组不一定恰好就是满的,比如elementData数组大小为10,而真正只存储了3个元素,那么为了提高序列化速度和减少序列化文件大小,程序只需要序列化有数据的3个元素,而不是整个elementData数组。为此,ArrayList重写了writeObject()方法:

    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();
        }
    }
    

    参考

    Java并发编程实战 P66-70
    Stack Overflow :why-is-java-vector-and-stack-class-considered-obsolete-or-deprecated
    五月的仓颉 :图解集合1:ArrayList

  • 相关阅读:
    xpage 获取 附件
    Win8.1应用开发之离线缓存
    分布式系统设计系列 -- 基本原理及高可用策略
    hdu3652(数位dp)
    HDU 4869 Turn the pokers
    ubuntu 安装mysql, 以及全然又一次安装的方法
    Wildcard matching
    并发编程:创建进程 join方法 进程间的数据隔离问题 进程对象以及其他方法 守护进程 互斥锁
    网络通信 : 粘包解决的实例 socketserver模块 udp的使用 简易版QQ的实现 异常处理机制的知识点
    TCP通信: scoket模块 黏包问题 连接循环 通信循环
  • 原文地址:https://www.cnblogs.com/bluemilk/p/10674382.html
Copyright © 2011-2022 走看看