ArrayList与LinkedList源码阅读

244 阅读6分钟

1. 前言

  了解过java或者说数据结构的人,应该都知道一个常识,ArrayList是由数组实现的,LinkedList是由双向链表实现的。可他们内部的具体逻辑是什么样子的呢,下面就分别分析一下这两个类的源码。

2.ArrayList

  ArrayList是由数组实现的一种集合,那为什么要使用ArrayList而不是直接使用数组呢?因为ArrayList支持泛型,并且它会自动扩容,所以不需要我们去关心初始化时应该定义多大容量的数组,会不会多了或者少了。当然如果非常确定要使用的数组的长度范围,还是要定义数组长度比较好,可以避免扩容时产生的性能消耗。   ArrayList的默认长度为10,如果有需要也可以自己通过构造函数定义

//默认长度
private static final int DEFAULT_CAPACITY = 10;

  ArrayList有两个成员变量:数组和size,为什么数组要用transient来标记呢?因为数组默认的序列化方法是会序列化数组当前容量下的所有元素,事实上,我们有用到的有效长度并没有那么多,所以ArrayList自己重写了序列化方法。

transient Object[] elementData;

private int size;

  接下来看看ArrayList的构造方法,它提供了三个构造方法给我们,无参、传入长度、传入集合

//可以自定义初始长度的构造方法
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

//默认初始长度为10的构造方法
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//将集合转化为ArrayList的构造方法
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

  然后看一下最常用的get方法

    public E get(int index) {
        rangeCheck(index);
        
        return elementData(index);
    }
    
    //检查是否超出size的范围,超出的话直接抛异常出去
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    //返回该索引的元素
    E elementData(int index) {
        return (E) elementData[index];
    }

  下面看一下add、addAll方法。
  add有两种方式,一种是在数组末尾插入,另一种是指定位置插入。无论是哪种方式,都要在插入之前判断一下当前数组是否已经装满了,如果装满了则执行扩容操作。在制定位置插入的话,需要把指定位置及之后的元素全部向后挪一位,然后再把元素插入指定位置。 &emsp addAll也有两种方式,一种是在数组末尾插入,另一种是指定位置插入,跟单个add的实现方式差不多,判断加上这一批元素后是否超出长度,超出则扩容。然后在末尾或者指定位置加入这一批元素。可以看到这里很容易会发生线程安全的问题,比方说在线程A判断完不需要扩容后,线程B刚好添加了一批元素,则会抛出异常。

    public boolean add(E e) {
    //判断是否扩容,如果需要则扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

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

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //将指定位置之后的元素全部向后挪移位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        //是否超出长度,超出则扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //将这一批元素放到数组结尾
        System.arraycopy(a, 0, elementData, size, numNew);
        //更新size
        size += numNew;
        return numNew != 0;
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        //判断是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
        //将指定位置的元素都向后挪numNew位
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
        //在指定位置插入这一批元素
        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

  然后看一下remove操作,remove操作也有两种方式,一种是根据索引删除,一种是根据元素删除。根据索引删除就不用说了,根据元素删除的实现逻辑是遍历数组,通过equal来找到元素,然后删除。

    public E remove(int index) {
    //判断索引是否小于size
        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;
    }

    public boolean remove(Object o) {
    //判断传入的参数是否为null,因为null调用equals方法会抛异常
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
        //根据euqals方法来找到要删除的元素,并且删除
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
        private void fastRemove(int index) {
        modCount++;
        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
    }

  最后还是看一下扩容操作,当添加元素后,长度比当前容量大时,会定义一个新的数组,长度为oldCapacity + (oldCapacity >> 1),然后将旧数组的元素复制到新数组上,所以说扩容操作会导致不必要的性能损耗。

    private void grow(int minCapacity) {
        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.LinkedList

  linkedList是由双向链表构成的,所以它有一个前驱和一个后继,并且定义了头节点和尾节点具体结构如下

    private static class Node<E> {
        E item;
        //后继
        Node<E> next;
        //前驱
        Node<E> prev;
    }
    
    //头结点
    transient Node<E> first;

    //尾结点
    transient Node<E> last;

  下面看一下get操作,没什么特殊的,就是先判断是否超出了长度,然后遍历链表,找到该索引的结点

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

  add操作就是在结尾插入一个结点,remove和poll都是返回并删除头结点。把它们放到一起的原因是,这三个恰巧是Queue接口的常用方法。remove和poll的区别是,若当前链表为空链表,remove会抛异常出去,而poll会返回null。

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
    }

    public E remove() {
        return removeFirst();
    }

4.后记

  跟HashMap比起来,ArrayList和LinkedList的源码要简单很多,但还是有一些细节的地方是要注意的。 1.ArrayList能定义长度的话尽量定义长度。
2.多线程下要注意线程安全的问题
3.LinkedList是双向链表,而不是单向链表
4.Queue接口是用LinkedList实现的。

本文只作为学习过程中的随笔,如果有不正确的地方欢迎指出。