Java容器源码分析—List

概述

本文主要参考了Java Collection Framework 源码剖析这位博主的专栏,写的很好,感兴趣的可以去看一下!

List 是 Java Collection Framework的重要成员,具体包括List接口及其所有的实现类。由于List接口继承了Collection接口,所以List拥有Collection的所有操作。

  • ArrayList是一个动态数组,实现了数组动态扩容,随机访问效率高;
  • LinkedList是一个双向链表,随机插入和删除的效率高,可以用作队列的实现;
  • Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但ArrayList是非线程安全的,而Vector是线程安全的;

List结构

ArrayList


1、ArrayList概览

public class ArrayList<E> extends AbstractList<E>
		implements List<E>,RandomAccess,Cloneable,java.io.Serializable

数组的初始容量为10

private static final int DEFAULT_CAPACITY = 10;
  1. ArrayList实现了List中的所有操作,允许包括NULL在内的所有元素;
  2. ArrayList实现了Serializable接口,支持序列化,能够进行序列化传输;
  3. ArrayList实现了RandomAccess接口,支持快速随机访问,就是通过数组下标进行快速访问;
  4. ArrayList实现了Cloneable接口,能被克隆;
  5. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用 Collections.synchronizedList(List l) 函数返回一个线程安全的ArrayList类,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

2、扩容方法

向 ArrayList 中增加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度。如果超出,ArrayList 将会进行扩容,以满足添加数据的需求;

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍;

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

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

//确保容量足够
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

//扩容操作+增大1.5倍
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    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);
}

3、元素的删除

ArrayList 共有 根据下标或者指定对象两种方式的删除功能;

首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将list末尾元素置空(null),返回被移除的元素;

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的;

public E remove(int index){
	rangeCheck(index);
	modCount++;
	E oldValue = elementData(index);
    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
    return oldValue;
}

4、Fail-Fast

动机: 在 Java Collection 中,为了防止在某个线程在对 Collection 进行迭代时,其他线程对该 Collection 进行结构上的修改

本质:Fail-Fast是Java集合的一种错误检测机制;

作用场景:在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException

假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会触发fail-fast机制,抛出 ConcurrentModificationException 异常

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

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

5、Fail-Safe

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原
有集合内容,在拷贝的集合上进行遍历;

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException

缺点:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的;

场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改;

6、序列化

ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。

保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化

transient Object[] elementData;

ArrayList的元素最终还是会被序列化的,在序列化/反序列化时,会调用 ArrayList 的 writeObject()/readObject() 方法,将该 ArrayList中的元素(即0…size-1下标对应的元素) 和 容量大小 写入流/从流读出;

好处:只保存/传输有实际意义的元素,最大限度的节约了存储、传输和处理的开销;

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

  • ArrayList 基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此 查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低;
  • 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。

Vector


Vector和ArrayList类似,区别在于Vector是同步类(synchronized),开销比ArrayList大,初始容量是10,实现了随机访问的接口,内部也是以动态数组的形式存储数据。

  • Vector可以设置增长的空间大小;
  • Vector增长为原来的一倍;
  • Vector线程同步;

1、同步

实现与ArrayList类似,但是使用synchronized进行同步

//采用synchronized进行同步
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

2、扩容

Vector 的构造函数可以传入 capacityIncrement 参数,它的作用是在扩容时使容量 capacity 增长 capacityIncrement。如果这个参数的值小于等于 0,扩容时每次都令 capacity 为原来的两倍

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    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);
}

没有capacityIncrement的构造函数时,capacityIncrement值被设置为0,默认情况下Vector每次扩容时容量都会翻倍;

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

public Vector() {
    this(10);
}

3、CopyOnWriteArrayList 类


可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类得到一个线程安全的ArrayList

List<String> list = new CopyOnWriteArrayList<>();

3.1 读写分离

  • 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响;

  • 写操作需要加锁,防止并发写入时导致写入数据丢失;

  • 写操作结束之后需要把原始数组指向新的复制数组。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

3.2 适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景;

弊端:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中;

CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景;

LinkedList


1、概览

  • LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作;

  • LinkedList 实现 List 接口,具有 List 的所有功能;

  • LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用;

  • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆;

  • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输;

  • 与 ArrayList 不同,LinkedList 没有实现 RandomAccess 接口,不支持快速随机访问

2、LinkedList数据结构

LinkedList数据结构

LinkedList底层的数据结构是基于双向链表的,且头结点中不存放数据,节点实例保存业务数据,前一个节点的位置信息和后一个节点位置信息;

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}
//每个链表存储了first和last指针
transient Node<E> first;
transient Node<E> last;

3、删除

删除过程可以分为以下三步:

  • 调整相应节点的前后指针信息
    e.previous.next = e.next;//预删除节点的前一节点的后指针指向预删除节点的后一个节点;
    e.next.previous = e.previous;//预删除节点的后一节点的前指针指向预删除节点的前一个节点

  • 清空预删除节点
    e.next = e.previous = null;
    e.element = null;

  • gc完成资源回收,删除操作结束

4、与ArrayList相比

  • ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
  • ArrayList 支持随机访问,LinkedList 不支持;
  • LinkedList 在任意位置添加删除元素更快

ArrayList,LinkedList 和Vector 面试题


1、Vector ArrayList LinkedList 的区别

这个看完博客就能了解到了,这里就不写了;


2、使用ArrayList的迭代器会有什么问题?单线程和多线程环境下;

常用的迭代器设计模式,iterator方法返回一个父类实现的迭代器;
1、迭代器的hasNext 方法的作用是判断当前位置是否是数组最后一个位置,相等为false,否则为true;
2、迭代器next 方法用于返回当前的元素,并把指针指向下一个元素,值得注意的是,每次使用next 方法的时候,都会判断创建迭代器获取的这个容器的计数器modCount 是否与此时的不相等, 不相等说明集合的大小被修改过, 如果是会抛出
ConcurrentModificationException 异常,如果相等调用get 方法返回元素即可;


3、Array和ArrayList有什么区别?

Array可以包含基本类型和对象类型,大小固定;
ArrayList 只能包含对象类型,大小是动态变化的,ArrayList 提供更多的方法和特性;


4、ArrayList和Vector的异同点?

相同点:

  1. 两者都是基于索引的,都是基于数组;
  2. 两者都维护插入顺序,可以根据插入顺序来获取元素;
  3. ArrayList 和Vector 的迭代器实现都是fail-fast 的;
  4. ArrayList 和Vector 两者允许null 值,也可以使用索引值对元素进行随机访问;

不同点:

  1. Vector 是同步,线程安全,而ArrayList 非同步,线程不安全。对于ArrayList,如果
    迭代时改变列表,应该使用CopyOnWriteArrayList
  2. ArrayList 比Vector 要快,它因为有同步,不会过载;
  3. 在使用上,ArrayList 更加通用,因为Collections 工具类容易获取同步列表和只读列表;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值