ArrayList和LinkedList对比(性能分析和实现等)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/GBStyle/article/details/81538373

做Android开发或者Java开发的,面试估计是逃不了这个问题,面试官特别喜欢问这个问题,大家估计心里也有点底,也能说上一些区别。但是总有面试官喜欢刁难,不停的问:“还有吗?”,“还有吗?”,搞得自己超级方。所以最近打算深入了解ArrayList和LinkedList的实现。

List接口

要说ArrayList和LinkedList的相同点的话,先看他们的继承关系
ArrayList
LinkedList
可以看到ArrayList和LinkedList都实现了List接口,也就是说ArrayList和LinkedList的不同实现。对于List接口,官方的描述为:

有序的 collection(也称为序列)。此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
与 set 不同,列表通常允许重复的元素。更确切地讲,列表通常允许满足 e1.equals(e2) 的元素对 e1 和 e2,并且如果列表本身允许 null 元素的话,通常它们允许多个 null 元素。难免有人希望通过在用户尝试插入重复元素时抛出运行时异常的方法来禁止重复的列表,但我们希望这种用法越少越好。

对于List接口的实现类,都有以下特点:

  • 允许重复的元素(但当出现重复元素的时候,操作的时候也要小心。当修改重复元素时,所有的重复的元素都会受影响)
  • 允许null值。至少ArrayList和LinkedList都允许有null值,并且null也是可以重复的,添加多个null,list的长度也会增加
  • 删除操作时,如果是根据对象删除的话,会删除第一个出现的元素。(这样如果数组内有多个重复元素的时候也不会混淆)

List接口内的抽象方法如下(来自jdk1.8,所以有些方法有默认实现,即default关键字修饰的方法)

public interface List<E> extends Collection<E> {
    // Query Operations

    //返回列表的大小,即元素的数量。如果元素的数量超过Integer.MAX_VALUE的话,则放回Integer.MAX_VALUE
    int size();

    //空的列表则返回true(注意,空列表是内部没有元素,而不是没有赋值)
    boolean isEmpty();

    //如果数组包含某个元素,则返回true,否则返回false
    boolean contains(Object o);

    //返回迭代器
    Iterator<E> iterator();

    //返回包含该列表所有元素的数组,该数组为重新分配的新数组,更改数组的元素不会影响原数组(这里说的是更改数组的某个元素,如果修改的是数组元素引用的对象,则会列表相应的元素也会被更改)
    Object[] toArray();

    <T> T[] toArray(T[] a);


    // Modification Operations

    //列表尾部添加
    boolean add(E e);

    //指定元素删除,删除的时第一个出现的元素,如果存在,则返回true
    boolean remove(Object o);

    // Bulk Modification Operations

    //判断列表是否包含某个集合的所有元素
    boolean containsAll(Collection<?> c);

    //列表尾部添加集合中所有的元素
    boolean addAll(Collection<? extends E> c);

    boolean addAll(int index, Collection<? extends E> c);
    
    boolean removeAll(Collection<?> c);
    
    //列表只保存存在此集合中的元素,删掉其他不在此集合的元素
    boolean retainAll(Collection<?> c);

    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
    
    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
    
    //清空列表中的所有元素
    void clear();


    // Comparison and hashing
    
    boolean equals(Object o);
    
    int hashCode();


    // Positional Access Operations

    //获取指定位置的元素
    E get(int index);
    
    //重新设置新的元素到指点的位置
    E set(int index, E element);

    void add(int index, E element);

    E remove(int index);


    // Search Operations

    int indexOf(Object o);
    
    int lastIndexOf(Object o);


    // List Iterators

    ListIterator<E> listIterator();

    ListIterator<E> listIterator(int index);

    // View
    
    List<E> subList(int fromIndex, int toIndex);
    
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.ORDERED);
    }
}

事实上,由于ArrayList和LinkedList都实现了List接口,所以它们都实现了上述的方法。它们两者的区别在于具体的实现方式不同,接下来就开始具体分析ArrayList和LinkedList的实现了。

ArrayList分析

ArrayList的实现为一个可变容量的数组,其中有一个数组用于保存列表中的元素,常数size用于标明当前元素的数量。即事实上,列表只包含索引在size之前的元素,其他位置不使用。当添加的元素超过了数组的容量的时候,就会进行扩容操作,一次扩容1.5倍,所以ArrayList在添加元素的时候,会有扩容代价(即复制数组的代价)
主要的成员变量

    //用于存放列表中的元素
    transient Object[] elementData;
    //当前里诶表的长度
    private int size;

数组扩容实现

    private void grow(int minCapacity) {
		 //elementData为ArrayList保存元素的数组
        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);
    }

下面为一些常用方法的分析(ArrayList实现了泛型,下面用E表示元素的类型):

  • add(E):直接在元素尾部添加元素,如果没有扩容,时间复杂度为O(1) 、
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
   
  • add(int,E):在某个位置插入元素,需要将该索引之前所有的元素向后移以为,然后将元素放到数组的某个位置,如果没有扩容,时间复杂度主要为复制数组的代价。
    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++;
    }
  • get(int):获取某个位置下的元素,时间复杂度为O(1)
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
  • set(int):类似于get,时间复杂度为O(1)
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
  • remove(int):删除某个位置上的元素,也是需要移动索引位置后面所有的元素
    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,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
  • remove(Object):移除某个元素,分两种情况:如果要移除的对象为空,则找到elementData中第一次出现空对象的位置,如果有,则返回true,否则返回false;如果要移除的对象不是空,那么则可以直接调用equals方法进行比较。在这里之所以这么麻烦分两步是因为,数组中有可能有空的对象,所以不能调用elementData.equals()方法,另外,传入的对象也可能为空,一样不能直接调用equals()方法。性能方面,该方法多出了一步是判断,并且需要遍历整个数组
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            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
    }
  • clear():清除数组中的所有元素
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

其实还要一个很重要的变量,那就是modCount,该变量定义在ArrayList的父类AbstractList内。所以在ArrayList找不到该变量定义的位置。该变量记录列表增删等列表结构发生变化的次数。主要用在迭代器上,用于实现迭代器的快速失败。

    //定义在AbstractCollection中
    protected transient int modCount = 0;

其实分析完ArrayList的方法,基本上都是靠一个数组和一个变量标志当前位置来实现的,结构很简单,再加上注释很清除,很容易看懂。其他还有些增删改一个集合的方法,在这里就不展开了,实现原理类似。

LinkedList分析

LinkedList的实现是通过一个双向链表链表来实现的,列表中的值存放在节点上,每个节点都有保存着其前一个节点和后一个一个节点的引用,可以方便的从任意一个节点遍历找到列表中的所有元素。另外,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;
        }
    }

LinkedList的源码实现也是相对较为简单。

  • add(E): 直接在链表的尾部添加一个元素,时间复杂度为O(1)
  • add(int, E): 需要找到索引的位置,然后再在链表中插入。在查找索引位置的时候,存放头结点和尾节点的变量就派上用场了,根据索引值和size大小的比较,判断是从头开始遍历还是从尾开始遍历(空间换时间,一个变量就可以减少一半的搜索操作了)。使用该方法,那么add方法的效率则要多出一部分花在查找元素上。
  • get(int): 和通过索引插入元素一样,都需要遍历一半的列表来找到元素。(为什么不是整个,就是因为有个头结点和尾节点)
  • set(int,E): 修改某个索引下的值,仍然需要找到元素之后再进行修改
  • remove(Object): 这个比较特殊,很多人认为这个的时间复杂度为O(1),其实不然,因为实际上的值是通过节点封装之后再存入到LinkedList上的,并不是直接存的,所以还要花费性能在查找元素上面。
  public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

另外,LinkedList实现了Deque接口,也就是实现了双端队列,其实也是队列,所以可以直接当做队列使用。
其他还有一些方法,也是很简单,可以自行观看源码


总结

其实ArrayList和LinkedList的实现都是很简单,一目了然,只要肯稍微花点时间就可以看完,防止面试过程中的问到。
另外再来简单总结下ArrayList和LinkedList:

  1. ArrayList使用可变数组实现,LinkedList使用双向链表使用
  2. ArrayList适合用于需要频繁查找和修改的环境,LinkedList则适合用于需要频繁增删的环境
展开阅读全文

没有更多推荐了,返回首页