Java集合四之List总结

一:List简介

  1. List接口

    一个 List 是一个元素有序的、可以重复、可以为 null 的集合(有时候我们也叫它“序列”)。

    Java 集合框架中最常使用的几种 List 实现类是 ArrayList,LinkedList 和 Vector。在各种 List 中,最好的做法是以 ArrayList 作为默认选择。 当插入、删除频繁时,使用 LinkedList,Vector 总是比 ArrayList 慢。

  2. 为什么 List 中的元素 “有序”、“可以重复”呢?

    首先,List 的数据结构就是一个序列,存储内容时直接在内存中开辟一块连续的空间,然后将空间地址与索引对应。

    从官方文档中我们也可以看出 :

    The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.

    可以看到,List 接口的实现类在实现插入元素时,都会根据索引进行排列。

    另一方面,由于 List 的元素在存储时互不干扰,没有什么依赖关系,自然可以重复。

  3. List和Array的区别

    List 在很多方面跟 Array 数组感觉很相似,尤其是 ArrayList和Vector,那 List 和数组究竟哪个更好呢?

    相似之处:

    都可以表示一组同类型的对象
    都使用下标进行索引
    

    不同之处:

    数组可以存任何类型元素
    List 不可以存基本数据类型,必须要包装
    数组容量固定不可改变;List 容量可动态增长
    数组效率高; List 由于要维护额外内容,效率相对低一些
    容量固定时优先使用数组,容纳类型更多,更高效。
    

    在容量不确定的情景下, List 更有优势,通过前三篇的源码介绍我们知道, ArrayList 、 LinkedList和Vector 都实现了容量动态增长。ArrayList 和Vector都有自己的扩容机制,ArrayList 默认初始容量为10,增长为 (size*3)/2+1 ;Vector默认初始容量为10,增长为 size*2,其中Vector更是可以自定义增量倍数。而LinkedList由于本身其实就是一个双向链式结构,所以其不存在元素个数限制。

二:ArrayList、LinkedList、Vector之间的比较

  1. 容器动态增长

    通过前几篇的源码讲解,我们可以看出:

    ArrayList和Vector都是使用Objec[]数组形式来存储数据。当你向这两种集合类型中添加元素的时候,如果元素的数目超出了内部数组目前的长度,它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度size*2,ArrayList则是增长为 (size*3)/2+1,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。其中Vector由于可以自定义增量倍数,所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小和倍数来避免不必要的资源开销。

    而LinkedList由于本身其实就是一个双向链式结构,所以其不存在元素个数限制。

  2. 增删改查操作

    ArrayList和Vector都是由数组存储数据,而LinkedList是双向链式结构。我们知道,数组从指定的位置检索一个对象,或在末尾插入、删除一个对象的时间是一样的,可表示为O(1)。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?因为当我们在对数组进行操作时,集合中第i和第i个元素之后的所有元素都要执行(n-i)个对象的位移操作,这是非常耗费时间的。

    LinkedList中,由于双向链式结构的特性,每个元素都只知道当前的前一个元素和后一个元素,在插入、删除集合中任何位置的元素时,只需要将所需删除、插入对象的前后元素所对应的前后进行重新定向就行,而无需进行移位操作,所花费的时间都是一样的—O(1);但它在索引一个元素的时候比较慢,为O(i),其中i是索引的位置。

    从本质上来说,它们之间进行增删改查操作的性能对比,就是在对 数组与链表之间的对比上。

  3. 线程安全

    通过前三篇的源码解析可以看出:

    Vestor中所有方法或直接或间接的使用了关键字 synchronized 进行加锁同步操作,显然他是线程安全的;在多线程的情况下,无疑使用Vector更佳。

    ArrayList,LinkedList是不同步的。如果不要求线程安全的话,可以使用ArrayList或LinkedList,可以节省为同步而耗费的开销。在多线程的情况下,也可以通过一些办法包装ArrayList,LinkedList,使他们也达到同步,但效率可能会有所降低。

    讲到这里,就不得不介绍一下CopyOnWriteArrayList类了,首先我们来看一个它的部分源码:

    public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
        private static final long serialVersionUID = 8673264195747942595L;
    
        //重入锁:递归无阻塞的同步机制
        transient final ReentrantLock lock = new ReentrantLock();
    
        //数组容器
        private volatile transient Object[] array;
    
        //无参构造函数,初始容器0
        public CopyOnWriteArrayList() {
            setArray(new Object[0]);
        }
    
        //将集合C放入初始容器中
        public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
        setArray(elements);
        }
    
        //通过获取数组长度来得到容器大小,和ArrayList的定义私有属性有所差别
        public int size() {
            return getArray().length;
        }
    
        //将index索引处值设为element,并返回原有值
        public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            Object oldValue = elements[index];
    
            if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
            } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
            }
            return (E)oldValue;
        } finally {
            lock.unlock();
        }
        }
    }

    通过它的私有属性我们发现,和ArrayList,LinkedList所不同的是,它不存在size属性,直接通过获取数组容器的长度来返回大小;但是却多了一个ReentrantLock对象;查看整个CopyOnWriteArrayList类的源码实现,其实它的所有方法都和ArrayList的实现一样。唯一不同的在于,在实现增删改操作时,方法体的前后都存在这样一段代码:

    
    final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            方法体
        } finally {
            lock.unlock();
        }

    显而易见,在对数据进行变更之前,CopyOnWriteArrayList中方法都通过调用ReentrantLock 类的lock()方法进行了加锁,并且在代码块执行结束之后立即释放了锁,以此达到了同步效果。

    那它和Vector之间性能对比如何呢?首先从ReentrantLock 这个类本身谈起了。

    我们来看看官方文档对其本身的解释:

    一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。

    从官方文档的解释中,ReentrantLock是一种递归无阻塞的同步机制。以前一直认为它是synchronized的简单替代,而且实现机制也不相差太远。不过最近实践和查看资料发现它们之间还是有着天壤之别。很明显,在加锁机制中,其处理机制明显要丰富于synchronized;在性能上也有较大的提升。

    其提供了lock()方法:

    如果该锁定没有被另一个线程保持,则获取该锁定并立即返回,将锁定的保持计数设置为 1。
    如果当前线程已经保持该锁定,则将保持计数加 1,并且该方法立即返回。
    如果该锁定被另一个线程保持,则出于线程调度的目的,禁用当前线程,
        并且在获得锁定之前,该线程将一直处于休眠状态,此时锁定保持计数被设置为 1。
    

    整体上来说,CopyOnWriteArrayList在多线程中性能优于Vector;单线程中ArrayList在检索上优于LinkedList,而在增删上LinkedList更胜一筹。

三:总结

  1. 如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
  2. 如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
  3. 实际方法中,尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值