ArrayList、LinkedList 源码解析

一、标准答案吧

ArrayList 是基于动态数组实现的,可以根据需要调整容量,扩容增加 50%。其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。

LinkedList 是双向链表结构,所以它不需要像上面两种那样调整容量。它进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。

ArrayList、LinkedList 为非线程安全。

二、集合框架结构

List,它提供了方便的访问、插入、删除等操作。

Set,Set 是不允许重复元素的,这是和 List 最明显的区别,也就是不存在两个对象 equals 返回 true。我们在日常开发中有很多需要保证元素唯一性的场合。

Queue/Deque,则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-First-Out)等特定行为。

Collection接口主要方法

    //Collection接口中主要方法
    int size();

    boolean isEmpty();
  
    boolean add(E e);
  
    boolean remove(Object o);

    void clear();

    //List接口中增加的方法
    E get(int index);

    E set(int index, E element);

三、ArrayList 源码

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

首先看下插入方法,重点要注意执行插入元素是超过当前数组预定义的最大值时,数组需要扩容。可以查看下 add、ensureCapacityInternal、ensureExplicitCapacity、grow 方法

    private void grow(int minCapacity) {
        // overflow-conscious code
        //把数组的长度赋给oldCapacity
        int oldCapacity = elementData.length;
        //新的数组容量=老的数组长度的1.5倍。oldCapacity >> 1 相当于除以2
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新的数组容量newCapacity小于传入的参数要求的最小容量minCapacity,那么新的数组容量以传入的容量参数为准。
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //如果新的数组容量newCapacity大于数组能容纳的最大元素个数 MAX_ARRAY_SIZE 2^{31}-1-8
            //那么再判断传入的参数minCapacity是否大于MAX_ARRAY_SIZE,如果minCapacity大于MAX_ARRAY_SIZE,那么newCapacity等于Integer.MAX_VALUE,否者newCapacity等于MAX_ARRAY_SIZE
        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);
    }

其中有几个判断不好理解,我已经注释上去。先把 MAX_ARRAY_SIZE 和之后方法中的 Integer.MAX_VALUE 列出来

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

    @Native public static final int   MAX_VALUE = 0x7fffffff;

再把方法 hugeCapacity 列出来

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

当重新计算的容量(x1.5那个计算)小于传入要求容量参数,则新容量以传入的比较大的容量参数为准。

当传入容量参数太大,大到超过了数组的容量限定值却又小于整数限定值 -1,那么新的数组容量以整数限定值 -1 为准,但是当传入的容量参数不大于数组的容量限定值时,以容量限定值为准。

接下来看下删除方法

    transient Object[] elementData;
    public E get(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        //直接取数组第index位置
        return (E) elementData[index];
    }

    
    public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        //取数组第index位置
        modCount++;
        E oldValue = (E) 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;
    }

删除元素时,如果 numMoved 计算出来大于 0,就需要调用 System.arraycopy 进行拷贝了,但是 remove 并不会减少数组的容量(如果需要缩小数组容量,可以调用 trimToSize() 方法),之后看下。

紧跟着了解下 get 和 set 方法

    public E get(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
    }

直接取出数组 index 位置下标,我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址。

    //其中 data_type_size 表示数组中每个元素的大小
    //base_address 首字节
    a[i]_address = base_address + i * data_type_size

比如,我们拿一个长度为 10 的 int 类型的数组 int[] a = new int[10] 来举例。计算机给数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。这个公式非常简单,我就不多做解释了。

下面给出set方法,一目了然了。

    public E set(int index, E element) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        
        E oldValue = (E) elementData[index];
        elementData[index] = element;
        return oldValue;
    }

 trimToSize方法,三目表达式,也是一目了然。

    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

四、LinkedList

我们肯定可以猜到 LinkedList 不会有容量大小的构造函数。既然它是一个双向链表,我们重点看下它的链表结构体。

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

代码中注释较多,不多解释了。

添加方法add,注意,LinkedList 更提供了 addFirst和removeFirst、addLast和removeLast、以及getFirst 和 getLast 等有效的添加、删除和访问表两端的项。

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

    void linkLast(E e) {
        // 获取尾部元素
        final Node<E> l = last;
        // 以尾部元素为前继节点创建一个新节点
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            // 链表为空,头节点first也是尾节点last了
            first = newNode;
        else
            // 不是空链表的情况,将原来的尾部节点的next指向需要插入的节点
            l.next = newNode;
        // 更新链表大小和修改次数,插入完毕
        size++;
        modCount++;
    }

addFirst。

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

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

remove。

    public E remove(int index) {
        checkElementIndex(index);
        //checkElementIndex与node不必说了,重点看下unlink
        return unlink(node(index));
    }

    E unlink(Node<E> x) {
        // 指定节点的值
        final E element = x.item;
        // 指定节点的后继节点
        final Node<E> next = x.next;
        // 定节点的前继节点
        final Node<E> prev = x.prev;

        // 如果prev为null表示删除是头节点,否则就不是头节点
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            // 置空需删除的指定节点的前置节点
            x.prev = null;
        }

        // 如果next为null,则表示删除的是尾部节点,否则就不是尾部节点
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        
        // 置空需删除的指定节点的值
        x.item = null;
        // 数量减1
        size--;
        modCount++;
        return element;
    }

而 get 和 set 就需要调用 node 遍历了。

    public E get(int index) {
        //检查以下,重点是node,它是查询指定位置元素并返回
        checkElementIndex(index);
        return node(index).item;
    }

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

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

五、《数据结构与算法分析》原文

ArrarList 的优点在于对 get 和 set 的调用花费常数时间。其缺点是新项的插入和现有项的删除代价昂贵,除非变动是在 ArrayList 的末端进行。

LinkedList 的优点在于新项的插入和现有项的删除均开销很小,这里假设变动项的位置是已知的。其缺点是它不容易作索引,因此对 get 的调用是昂贵的,除非调用非常接近表的端点(如果对 get 的调用是对接近表后面的项进行,那么搜索的进行可以从表的后面开始)。

关于ArrayList、LinkedList 中 ListIterator 接口,这里不多赘述了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值