深入浅出的理解ArrayList、LinkedList和Vector的底层实现原理


1. List接口

List属于单列集合,常用来替换数组使用。集合类中元素有序且可重复,每个元素都有其对应的顺序索引,可以根据指定的索引在集合中存取元素。List接口常用的实现类有:

  • ArrayList
  • LinkedList
  • Vector

关于List接口各个实现类的使用已经在浅析Java中的List接口和实现类中有过介绍,这里着重看一下它们底层的实现原理。下面以JDK 8为基础进行源码的解读, 其中,每个实现类的原理分析都从字段构造方法常用方法三个角度进行。


2. ArrayList分析

ArrayList是List接口一种典型的、重要的实现类,本质上它就是对象引用的一个变长数组,如何理解呢?

2.1 字段

ArrayList中字段的定义如下:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, 		Cloneable, java.io.Serializable
{
    // 序列化ID,用于序列化和反序列化
    private static final long serialVersionUID = 8683452581122892189L;

    // 初始容量,默认为10
    private static final int DEFAULT_CAPACITY = 10;

    // 下面定义了两个空的数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	
    // 保存数据的数组,就是一个Object数组
    transient Object[] elementData; // non-private to simplify nested class access
	
    // 数组已保存数据的个数,默认就是0,这里并没有直接初始化一个固定大小的数组
    private int size;
    
    // 数组的最大容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

从字段的定义中可以看出,ArrayList底层就是使用了一个Object数组来保存数据。同时采用了懒汉式的模式,一开始并没有初始化一个固定容量的数组,而只是创建了一个空数组。

2.2 构造函数

ArrayList提供了三种构造函数供对象的实例化操作,源码如下:

// 无参构造,此时默认创建的是一个空数组
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 带参构造,参数为指定的数组容量大小
public ArrayList(int initialCapacity) {
    // 如果传入的参数大于0,则直接创建一个指定容量的数组
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    // 如果参数为0,那么同样只创建一个空数组
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 否则抛出异常,即必须保证参数大于等于0
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

// 使用另一个Collection接口的对象初始化
public ArrayList(Collection<? extends E> c) {
    // 首先将传入的对象转换为数组形式
    elementData = c.toArray();
    // 如果数组中有数据
    if ((size = elementData.length) != 0) {
        // 如果此时并不是Object类型数组
        if (elementData.getClass() != Object[].class)
            // 将其转换为Object类型,再赋给elementData
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果已经是Object类型,直接赋给elementData即可
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

下面通过一个例子看一下如何使用不同的构造函数进行对象实例化,如下所示:

@Test
public void test1(){
    List<String> list = new ArrayList<>();
    System.out.println(list.size());
    System.out.println("------------------");

    List<String> list1 = new ArrayList<>(10);
    System.out.println(list.size());
    System.out.println("------------------");

    List<String> help = new ArrayList<>();
    Collections.addAll(help, "Forlogen", "Kobe", "James");
    List<String> list2 = new ArrayList<>(help);
    System.out.println(list2.size());
    System.out.println(list.toString());
}

输出为:

0
------------------
0
------------------
3
[]
2.3 常用方法

下面我们主要看一下add()set()get()这几个方法的具体实现。

2.3.1 add

add方法有两种重载形式,其中一种是只传入要添加的元素,如下所示:

public boolean add(E e) {
    // 首先检查添加一个元素后是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 然后将其添加到数组的末尾,同时更新size
    elementData[size++] = e;
    return true;
}

其中ensureCapacityInternal()的实现如下:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

它又首先调用calculateCapacity)()来计算当前的elementData数组能否满足添加一个元素后所需的容量。

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果elementData此时为默认的空数组,那么返回初始容量和添加后容量的最大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
	
    // 如果不为空,直接返回所需的容量
    return minCapacity;
}

当得到了数组的容量后,再调用ensureExplicitCapacity()进行检查:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
	
    
    // 这里调用grow方法来创建所需的数组
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

其中grow()方法的实现如下:

private void grow(int minCapacity) {
    // 首先获取旧数组的长度
    int oldCapacity = elementData.length;
    // 以当前数组大小的1.5倍去扩容
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果扩容后仍不够,直接使用所需的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果扩容后的容量大小超过数组的最大长度,使用Integer的最大值或是MAX_ARRAY_SIZE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 确定了容量后才真正的创建数组elementData
    elementData = Arrays.copyOf(elementData, newCapacity);
}

从上面的分析可知,ArrayList底层非空数组的真正创建是在第一次添加数据时才进行。

另外一种是在指定的位置添加元素的实现,源码实现如下:

public void add(int index, E element) {
    // 首先检查指定的索引是否越界,如果越界直接抛异常
    rangeCheckForAdd(index);
	
    // 判断是否需要扩容
    ensureCapacityInternal(size + 1); 
    // 这里调用的System中的arraycopy来进行数据的添加
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 添加数据,更新size
    elementData[index] = element;
    size++;
}
2.3.2 set

set()用于将指定索引处的元素替换为指定的元素,它的实现如下:

public E set(int index, E element) {
    // 首先检查传入的索引是否合法
    rangeCheck(index);

    E oldValue = elementData(index);
    // 替换
    elementData[index] = element;
    return oldValue;
}
2.3.3 get

get()用于获取指定位置的元素,实现如下:

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

它同样是先检查传入的索引是否合法,如果不合法直接抛异常,合法则返回对应的元素。


3. LinkedList分析

ArrayList集合中元素允许重复,且元素之间是无序的。如果希望集合中的元素按照插入的顺序排列,以及对于频繁的插入或删除元素 的操作,LinkedList更为适合。LinkedList的实现形式为双向链表,它的内部实现采用的是内部类Node,将其作为存储数据的基本数据结构,而不是数组。Node的定义如下:

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

Node类不仅可以保存数据本身,而且还定义了next和prev来指向下一个元素和上一个元素。

3.1 字段

LinkedList中字段的定义如下,只有三个字段:

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    // 已保存数据的个数
    transient int size = 0;
	
    // 指向首个元素的指针
    transient Node<E> first;

    // 指向最后一个数据的指针
    transient Node<E> last;
3.2 构造方法

LinkedList中只有两个构造方法,如下所示:

public LinkedList() {}

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

一个无参构造和一个使用已有的Collection实现类对象进行构造的方法。

3.3 常用方法
3.3.1 add

add()同样有两种重载形式,其中不指定索引位置的实现如下:

public boolean add(E e) {
    linkLast(e);
    return true;
}

这里直接调用的是linkLast(),将其默认添加到LinkedList的末尾。它的方法实现如下:

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    // 如果此时为空,那么直接将其作为第一个节点
    if (l == null)
        first = newNode;
    // 否则将其添加到末尾
    else
        l.next = newNode;
    size++;
    modCount++;
}

另一种指定位置的实现如下:

public void add(int index, E element) {
    // 检查索引是否合法
    checkPositionIndex(index);
	
    
    // 如果指定的是size位置
    if (index == size)
        //和add(E e)的执行逻辑相同
        linkLast(element);
    else
        // 否则插入到指定位置
        linkBefore(element, node(index));
}

其中linkBefore()的实现逻辑如下所示:

void linkBefore(E e, Node<E> succ) {
    // 指定位置节点的前一个节点
    final Node<E> pred = succ.prev;
    // 使用要要添加的元素构造的Node节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 将其作为当前位置的prev指向的节点
    succ.prev = newNode;
    // 如果为空,直接作为首个节点
    if (pred == null)
        first = newNode;
    else
        // 否则成功插入
        pred.next = newNode;
    size++;
    modCount++;
}

整体的执行过程如下所示:

LinkedList指定位置插入

另外还有头插和尾插两种特殊的添加数据的实现,

public void addFirst(E e) {
    linkFirst(e);
}

public void addLast(E e) {
    linkLast(e);
}

它们分别调用了linkFirst()linkLast()linkLast()前面已经说完,它的执行过程如下:

LinkedList尾部插入

重点看一下linkFirst()的实现,如下所示:

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

实现方式大同小异,如果此时为空,直接作为头节点,否则将其添加到头部。具体的执行过程如下:

LinkedList头部插入

3.3.2 get

get()的实现如下:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

先检查索引是否合法,再取出节点的元素值。

3.3.3 set

方法实现如下:

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

同样的是先检查索引是否合法,然后再取出指定位置的节点进行值的替换。

3.3.4 remove

remove()的实现就是一个断链的过程,具体实现如下:

public E remove(int index) {
    // 检查索引
    checkElementIndex(index);
    return unlink(node(index));
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    //当前节点的后继
    final Node<E> next = x.next;
    //当前节点的前驱
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        //更新前驱节点的后继为当前节点的后继
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        //更新后继节点的前驱为当前节点的前驱
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

具体的执行过程如下所示:

LinkedList删除元素

图片来自宋红康老师的笔记资料。

3.4 总结

LinkedList的操作本质上就是底层双向链表的操作,如果理解链表的相关操作,理解LinkedList就很容易了。


4. vector

Vector基本上和ArrayList是相同的,唯一区别在于Vector是强同步类,所谓强同步,就是方法上直接使用了synchronized进行同步。下面着重看一下它个ArrayList实现上的不同之处。

字段方面它有一个capacityIncrement,作用就是在扩容时指定扩容的大小。构造方法如下:

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    // 创建指定容量的Object数组
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

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

public Vector() {
    this(10);
}

public Vector(Collection<? extends E> c) {
    elementData = c.toArray();
    elementCount = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}

底层数组默认大小为10,但是这里在构造方法中就直接初始化了指定容量大小的数组。另外一个不同之处就是扩容的实现,如下所示:

private void grow(int minCapacity) {

    int oldCapacity = elementData.length;
    // 如果指定了capacityIncrement,那么只增加相应的大小
    // 否则扩容为当前容量的2倍
    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有没有设置初始值,如果没有,扩容为当前的2倍;如果有,增加capacityIncrement大小的容量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值