Java集合之List

1,239 阅读6分钟

1、Collection

  • List,有序集合,它提供了方便的访问、插入、删除等操作。
  • Set,不允许重复元素的,这是和 List 最明显的区别,也就是不存在两个对象 equals 返回 true。
  • Queue/Deque,则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似【先入先出(FIFO, First-in-First-Out)】或者【后入先出(LIFO,Last-In-First-Out)】等特定行为。

1-1、ArrayList

数组结构的列表,非线程安全;

1-2、LinkedList

链表结构的列表,非线程安全;

1-3、Vector

数组结构的列表,线程安全;

1-4、ArrayList与LinkedList区别

Vector 是 【线程安全(方法使用synchronized修饰)】的动态列表。Vector 内部是使用【对象数组】来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。扩容时会提高 1 倍

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // capacityIncrement默认为0,所以扩容时时提高1倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量是增加 50%

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // oldCapacity 向右位移1位,代表除2,扩容0.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);
}

LinkedList 是 Java 提供的【双向链表】,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

适用的场景

  • VectorArrayList 作为【动态数组】,其内部元素以【数组】形式【顺序存储】的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。
  • LinkedList 进行节点插入、删除却要高效得多,但是【随机访问】性能则要比【动态数组】慢。

数据结构

扩容

线程安全

插入/删除时间复杂度

访问时间复杂度

Vector

数组

1倍

不安全

O(n)

O(1)

数组随机访问

ArrayList

数组

0.5倍

不安全

O(n)

O(1)

数组随机访问

LinkedList

双向链表

无需扩容

不安全

O(1)

O(n)

1-5、Set

  • TreeSet 支持自然顺序访问,但是添加、删除、包含等操作要相对低效(log(n) 时间)。
  • HashSet 则是利用哈希算法,理想情况下,如果哈希散列正常,可以提供常数时间的添加、删除、包含等操作,但是它不保证有序。
  • LinkedHashSet,内部构建了一个记录插入顺序的双向链表,因此提供了按照插入顺序遍历的能力,与此同时,也保证了常数时间的添加、删除、包含等操作,这些操作性能略低于 HashSet,因为需要维护链表的开销。

在遍历元素时,HashSet 性能受自身容量影响,所以初始化时,除非有必要,不然不要将其背后的 HashMap 容量设置过大。而对于 LinkedHashSet,由于其内部链表提供的方便,遍历性能只和元素多少有关系。

1-6、ArrayList如何实现【线程安全】?

java.util.Collections 的工具类内部的【synchronizedList(List list)】方法实现ArrayList线程安全,将每个基本方法,比如 get、set、add 之类,都通过 synchronized 添加基本的同步支持,非常简单粗暴,但也非常实用。

注意这些方法创建的线程安全集合,都符合迭代时【fail-fast 行为】,当发生意外的并发修改时,尽早抛出【ConcurrentModificationException】异常,以避免不可预计的行为。

public static <T> Collection<T> 
                      synchronizedCollection(Collection<T> c) {
    return new SynchronizedCollection<>(c);
}

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}


static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {


    final List<E> list;


    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }


    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {return list.equals(o);}
    }
    
    public E get(int index) {
        synchronized (mutex) {
            return list.get(index);
        }
    }
    
    public E set(int index, E element) {
         synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
         synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
         synchronized (mutex) {return list.remove(index);}
    }
   

1-7、ConcurrentModificationException

根据ConcurrentModificationException的文档介绍,**一些对象【不允许并发修改】,**当这些修改行为被检测到时,就会抛出这个异常。(例如一些集合不允许一个线程一边遍历时,另一个线程去修改这个集合)。

集合(例如Collection, Vector, ArrayList, LinkedList, HashSet, Hashtable, TreeMap, AbstractList)的【Iterator】实现中,如果提供这种【并发修改异常检测】,那么这些Iterator可以称为是"fail-fast Iterator",意思是【快速失败】迭代器,就是检测到并发修改时,直接抛出异常,而不是继续执行,等到获取到一些错误值时在抛出异常。

异常检测主要是通过【modCount】和【expectedModCount】两个变量来实现

modCount 集合被修改的次数,一般是被集合(ArrayList之类的)持有,每次调用add(),remove()方法会导致 modCount+1;

expectedModCount 期待的修改次数,一般是被Iterator(ArrayList.iterator()方法返回的iterator对象)持有,一般在Iterator初始化时会赋初始值,在调用Iterator的remove()方法时会对expectedModCount进行更新。

然后在Iterator调用next()遍历元素时,

会调用 checkForComodification() 方法

比较 modCount 和 expectedModCount,

不一致就抛出 ConcurrentModificationException。

单线程操作Iterator不当时也会抛出ConcurrentModificationException异常。(上面的例子就是);

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;


    Itr() {}


    public boolean hasNext() {
        return cursor != size;
    }


    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

总结

因为ArrayList和HashMap的Iterator都是上面所说的**“fail-fast Iterator”**,Iterator在获取下一个元素,删除元素时,都会比较expectedModCount和modCount,不一致就会抛出异常

所以当【使用Iterator遍历】元素(for-each遍历底层实现也是Iterator)时,需要删除元素,一定需要使用 Iterator的remove()方法 来删除,而不是直接调用ArrayList或HashMap自身的remove()方法,否则会导致Iterator中的expectedModCount没有及时更新之后获取下一个元素或者删除元素时,expectedModCount和modCount不一致,然后抛出ConcurrentModificationException异常。

1-7-1、迭代器与ConcurrentModificationException

目的:在【并发环境】下容器被修改是快速失败,保证容器安全和数据一致;

实现方式:计数器,记录容器变化的次数,如果在【迭代期间】计数器被修改,hasNext或next将抛出 ConcurrentModificationException。

在设计【同步容器类】的迭代器时,并没有考虑到【并发修改】问题,表现出的行为是【及时失败(fail-fast)】。容器在迭代过程被修改时,就会抛出ConcurrentModificationException异常。

1-8、集合框架中 fast-fail 和 fast-safe 设计

Java1.0 - Java1.4 集合框架设计是 fast-fail

Java1.5 开始,增加 java.util.concurrent ,增加并发安全框架 fast-safe