2.Java数据结构原理解析-List系列

一、List家族特点

集合效率线程安全性
ArrayList读取快,插入慢线程不安全
LinkedList插入快,读取慢线程不安全
Vector线程安全
CopyOnWriteArrayList读取快,插入慢线程安全

二、ArrayList

Java中的数组初始化后,长度就是不可变。简单来说,ArrayList就是可变长数组。
由于ArrayList底层是通过数组来实现的,所以必然要解决下面两个问题。

  1. 初始容量,最大容量
  2. 如何扩容

ArrayList的初始容量不是在初始化时设置的,而是在第一次进行add后者addAll操作时设置的。(这是JDK1.7做的一个优化,老版本中ArrayList在构造完成后容量默认是10)

下面是ArrayList的构造函数,在初始化时,并没有为数组分配空间

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

//存储数据的数组
private transient Object[] elementData;

public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
}

public ArrayList() {
    super();
    //初始化时,是空数组
    this.elementData = EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    if (elementData.getClass() != Object[].class)
        //将elementData拷贝到一个长度为size的新数组
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

下面是为ArrayList每次进行add或者addAll操作时分配内存的代码。

private void ensureCapacityInternal(int minCapacity) {
    //分配的最小容量=max(默认容量10,第一次插入的元素的个数)
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    //当数组的长度不够,调用grow方法对数组进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * Increases the capacity to ensure that it can hold at    least the number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //新的容量=以前的容量+以前容量的1/2
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    //如果newCapacity 比minCapacity 还要小,就分配minCapacity 
    //这里存在两种情况,一是newCapacity确实比minCapacity小,二是newCapacity 越界了,超过了int的最大值,导致newCapacity 为负数
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //newCapacity超过MAX_ARRAY_SIZE时,根据minCapacity来判断是分配MAX_ARRAY_SIZE还是Integer.MAX_VALUE
        newCapacity = hugeCapacity(minCapacity);

    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

int newCapacity = oldCapacity + (oldCapacity >> 1);这句执行后如果超过int的最大值,那么newCapacity会是一个负数。

总结一下ArrayList的扩容算法:
当前容量:oldCapacity
新的容量:newCapacity
此次分配需要的最小容量:minCapacity

当容量不足时,newCapacity=oldCapacity+oldCapacity*1/2,也就是oldCapacity的3/2倍。如果newCapacity发生越界或者newCapacity< minCapacity,那么newCapacity=minCapacity。如果minCapacity>MAX_ARRAY_SIZE,那么最终分配的容量为Integer.MAX_VALUE 或者MAX_ARRAY_SIZE;

三、LinkedList

LinkedList除了实现List接口外,还实现了Deque接口,也就是说,LinkedList除了作为集合外,还可以用作队列。
由于我们讨论的是List家族,所以,下面主要介绍LinkedList作为List中的成员,是如何实现的。

我们都知道,相对于ArrayList,LinkedList有插入更新快、读取慢的特点,这种特性与它底层的数据结构密切相关。

LinkedList底层的数据结构是双向链表。
这里写图片描述
注:LinkedList最开始的数据结构是双向循环链表(只有一个header指针),后面改成了双向链表(一个first指针、一个last指针)

public class LinkedList<E>
{
    transient int size = 0;
    //头节点
    transient Node<E> first;
    //尾节点
    transient Node<E> last;

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}

下面,我们来看看LinkedList插入快,查找慢。
下面是LinkedList插入元素时的代码,可以看出,插入一个元素时newNode时,只需要O(1)的时间复杂度。

public void addLast(E e) {
    //插入链表尾部
    linkLast(e);
}

void linkLast(E e) {
    final Node<E> l = last;
    //新增一个节点,前继为尾节点,后继为null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

再看看LinkedList在指定位置插入元素的代码,该操作的复杂度主要在node(int index)方法上,最坏的情况下,会循环size*1/2次,但是相对于ArrayList 的整体后移,还是相当快的。

public void add(int index, E element) {
    //校验是否越界
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        //将element插入node节点
        linkBefore(element, node(index));
}

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

//找到位置index的node节点,如果index<size*1/2,从前往后找,否则从后往前找
Node<E> node(int index) {
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

四、CopyOnWriteArrayList

  Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。
  CopyOnWrite容器即写时复制的容器。通俗地理解是当我们往一个容器中添加元素的时候,不直接往容器中添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新容器。这样做的好处是我们可以对CopyOnWrite容器就行并发的读,而不需要加锁。

1、CopyOnWriteArrayList实现原理

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

2、CopyOnWriteArrayList的应用  
  CopyOnWriteArrayList在JDBC的DriverManager中有使用,用于保存所有注册的驱动。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值