ArrayList 与 LinkedList

前言

ArrayList 与 LinkedList 估计在java面试汇总肯定会问到,一般需要掌握的点有.
比如还应该了解的有:

  1. 都是List,都java集合Collection接口的实现类.
  2. ArrayList基于数组实现,长度固定,但可以扩容.
  3. ArrayList查询快,增删慢,因为需要移动数据
  4. LinkedList增删快,不用移动数据
  5. LinkedList 提供了更多的方法来操作其中的数据,但是这并不意味着要优先使用LinkedList ,因为这两种容器底层实现的数据结构截然不同。
  6. 二者线程多不安全.为什么?

所以本文将试图去源码上找答案.

ArrayList

ArrayList是可变长度数组-当元素个数超过数组的长度时,会产生一个新的数组,将原数组的数据复制到新数组,再将新的元素添加到新数组中。

方法及源码

  • add
    //底册Object类型的数组
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) { 
        ensureCapacityInternal(size + 1);  // 扩容
        elementData[size++] = e;  //扩容后直接index赋值
        return true;
    }

先看是否越界,越界就扩容,
扩容源码如下

    private void grow(int minCapacity) {
        // overflow-conscious code
        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); //通过拷贝放入新数组进行扩容
    }

这里Arrays.copyOf 底层使用System.arraycopy拷贝到新数组(len+1)里.

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

        return elementData(index);
    }

get直接通过index获取值,so快.

  • 修改
    public E set(int index, E element) {
        rangeCheck(index);

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

没啥好讲的,直接覆盖.

  • 删除
    public E remove(int index) {
        rangeCheck(index); //检查边界

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, //拷贝new数组
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

删除后把index+1前移.然后末尾置为null,这里注释交给GC不太懂. 有一次拷贝和移动数组.

线程不安全

  • 为什么会线程不安全
  1. 因为size++属于非原子操作,这里在多线程下可能会被其他线拿到,又没有做扩容处理 可能会值覆盖或者越界即ArrayIndexOutOfBoundsException.
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true; 
    }
  1. 如果两个线程擦操作同一个下标数据,可能会覆盖. 原因是a线程已经扩容了,b线程没有get到size的变化,直接坐了覆盖操作.
  • 如何保证线程安全

首先理解线程安全,就是保证多个线程下数据

  1. Collections.synchronizedList
    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }


        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);} //同步
        }

但是源码注释有写到,遍历集合的时候需要手动同步

     * It is imperative that the user manually synchronize on the returned
     * list when iterating over it:
     * <pre>
     *  List list = Collections.synchronizedList(new ArrayList());
     *      ...
     *  synchronized (list) {
     *      Iterator i = list.iterator(); // Must be in synchronized block
     *      while (i.hasNext())
     *          foo(i.next());
     *  }
     * </pre>

所以为了防止忘记,建议第二种.

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

LinkedList

LinkedList 底层的数据结构是基于双向循环链表的,且头结点中不存放数据,如下:
image

  • add
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    
    
    /**
     * Links e as last element.
     */
    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++;
    }

  • remove
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

    /**
     * Unlinks non-null node x.
     */
    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;  // 将前一节点的next引用赋值为x的下一节点
            x.prev = null; //解除了x节点对前节点的引用
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev; // 将x的下一节点的previous赋值为x的上一节点
            x.next = null; //解除了x节点对后节点的引用
        }

        x.item = null; // 将被移除的节点的内容设为null
        size--;
        modCount++;
        return element;
    }

删除过程基本上就是:

  1. 前节点指向后节点
  2. 当前pre为空
  3. 后节点的pre指向前节点
  4. 当前节点next为空
  5. 当前节点为空 交给gc处理
  6. size--

与ArrayList比较而言,LinkedList的删除动作不需要“移动”很多数据,从而效率更高。

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

    Node<E> node(int index) {
        // assert isElementIndex(index);
       // 获取index处的节点。    
        // 若index < 双向链表长度的1/2,则从前先后查找;    
        // 否则,从后向前查找。  
        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;
        }
    }

index与长度size的一半比较,要么从头开始,要么从尾部开始. 但是都需要挨个查找,所以这里体验出LinkedList比ArrayList查询效率慢了

多线程下

LinkedList源码中发现,同样会出现非原子性操作 size++/size--问题,所以是线程不安全的.

  1. Collections.synchronizedList 通ArrayList同步方法
  2. ConcurrentLinkedQueue 这个源码下详解

总结

存储结构

ArrayList是基于数组实现的;按照顺序将元素存储(从下表为0开始),删除元素时,删除操作完成后,需要使部分元素移位,默认的初始容量都是10.

LinkedList是基于双向链表实现的(含有头结点)。

线程安全性

ArrayList 与 LinkedList 因为都不是原子操作,都不能保证线程安全.
Collections.synchronizedList,但是迭代list需要自己手动加锁

ps:
Vector实现线程安全的,即它大部分的方法都包含关键字synchronized,但是Vector的效率没有ArraykList和LinkedList高。

效率

ArrayList 从指定的位置检索一个对象,或在集合的末尾插入、删除一个元素的时间是一样的,时间复杂度都是O(1)

LinkedList中,在插入、删除任何位置的元素所花费的时间都是一样的,时间复杂度都为O(1),但是他在检索一个元素的时间复杂度为O(n).

ps: 事实上lindlinst并不会节省空间,只是相比增删节省时间而已.

posted @ 2020-09-10 16:25  dadongx  阅读(128)  评论(0编辑  收藏  举报