集合操作(一)ArrayList,LinkedList源码分析

一世执手 提交于 2020-01-07 17:33:16

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

ArrayList:

构造函数:

ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。

ArrayList底层是使用一个Object类型的数组来存放数据的。

transient Object[] elementData; // non-private to simplify nested class access

size变量代表List实际存放元素的数量

private int size;

不指定ArrayList大小时,默认数组大小为10

private static final int DEFAULT_CAPACITY = 10;

接下来看几个常用的方法:

add:

1.

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 扩容检测
    elementData[size++] = e; //新增元素加到末尾
    return true;
}

2.

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  //扩容检测
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index); //使用System.arraycopy的方法,将index后面元素往后移动1位
    elementData[index] = element;  // 存放元素到index位置
    size++;
}

set,get

get和set方法,都是通过数组下标,直接操作数据的,时间复杂度为O(1)

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

我们重点关注ArrayList的扩容策略,这也是我们在实际工作中决定是否选择ArrayList需要考虑的

像之前add方法里,每次增加元素时都会进行扩容检测,如果数组大小不足,则会自动扩容;如果扩容后的大小超出数组最大的大小,则会抛出异常。

ensureCapacityInternal(size + 1); // 扩容检测

那么ArrayList最大长度可以看一下定义,是Integer的最大长度-8,Integer是4个字节,那么ArrayList的最大长度就是2^32-8

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

扩容:

接下来看一下具体的扩容过程:

ArrayList扩容方案,主要有两个步骤:1.大小检测,2.扩容

大小检测

检测数组大小是否为0,如果是,则使用默认的扩容大小10

检测是否需要扩容,只有当数组最小需要容量大小大于当前数组大小时,才会进行扩容

扩容:grow和hugeCapacity

进行数组越界判断

拷贝原始数据到新的数组中

private void ensureCapacityInternal(int minCapacity) {
    // 通过ArrayList<Integer> a = new ArrayList<Integer>()或者通过序列化读取,元素大小为0时,底层数组才会为null数组
    // 如果底层数组大小为0,则使用默认的容量大小10
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    // 数据结构发生改变,和fail-fast机制有关,在使用迭代器过程中,只能通过迭代器的方法(比如迭代器中add,remove等),修改List的数据结构,
    // 如果使用List的方法(比如List中的add,remove等),修改List的数据结构,会抛出ConcurrentModificationException
    modCount++;  


    // 当前数组容量大小不足时,才会调用grow方法,自动扩容
    if (minCapacity - elementData.length > 0)
        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) //溢出判断,比如minCapacity = Integer.MAX_VALUE / 2, oldCapacity = minCapacity - 1
        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);
}

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

fail-fast机制的实现

可以看到在ArrayList中,有个modCount的变量,每次进行add,set,remove等操作,都会执行modCount++。

在获取ArrayList的迭代器时,会将ArrayList中的modCount保存在迭代中,

每次执行add,set,remove等操作,都会执行一次检查,调用checkForComodification方法,对modCount进行比较。如果迭代器中的modCount和List中的modCount不同,则抛出ConcurrentModificationException

final void checkForComodification() {
    if (expectedModCount != ArrayList.this.modCount)
        throw new ConcurrentModificationException();
}

在对集合进行迭代过程中,除了迭代器可以对集合进行数据结构上进行修改,其他的对集合的数据结构进行修改,都会抛出ConcurrentModificationException错误。

序列化

transient Object[] elementData;

transient修饰符让elementData无法自动序列化,这样的原因是,数组内存储的的元素其实只是一个引用,单单序列化一个引用没有任何意义,反序列化后这些引用都无法在指向原来的对象。ArrayList使用writeObject()实现手工序列化数组内的元素。

通过以上源码分析,其实就可以总结出ArrayList的优缺点以及使用场景

优点:

1.get,set,时间复杂度为O(1),查找元素快速

2.数据是顺序存储的

缺点:

1.add(一般都是在末尾插入),时间复杂度为O(1),最差情况下(往头部插入数据),时间复杂度O(n)

2.remove,时间复杂度为O(n),最优情况下(移除末尾元素),时间复杂度为O(1)

3.ArrayList底层使用数组存储数据,数组是不能自动扩容的,因此在发生扩容的情况下,需要移动大量的元素。

4.数组大小是由限制的,受jvm和机器的影响,当扩容超出上限时,ArrayList会抛出异常

5.ArrayList所有的操作,都不是同步的,因此ArrayList不是线程安全的。

使用场景:

插入操作多,数据量不大,顺序存储时,可以考虑使用ArrayList

LinkedList

构造函数:

LinkedList有两个构造参数,一个为无參构造,只是新建一个空对象,第二个为有参构造,新建一个空对象,然后把所有元素添加进去。

public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList的存储单元为一个名为Node的内部类,包含pre指针,next指针,和item元素,实现为双向链表

//链表的节点个数
transient int size = 0;

//指向头节点的指针
transient Node<E> first;

//指向尾节点的指针
transient Node<E> last;

节点结构

Node 是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,包括一个数据域item,一个后置指针next,一个前置指针prev。

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

 

总结:

ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方:

1.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。

2.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。

使用场景:

当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!