知识点(6)——LinkedList和ArrayList的效率差异性分析

前言

最近在看集合这一块,都知道LinkedList是双链表结构,可以用来实现栈、队列、双端队列(因为LinkedList实现了Deque,deque 即双端队列,是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行)。LinkedList随机访问效率低,但是插入、随机删除速度快。

而ArrayList是动态数组结构,实现了RandomAccess类。它的随机访问效率高,但是插入、随机删除速度慢。

下面我们来一起分析下为什么(下面的源码是基于JDK1.8)

 

分析1:为什么LinkedList中插入元素很快,而ArrayList中插入元素很慢

一、首先看LinkedList:

 LinkedList<String> linkedList=new LinkedList<>();
 linkedList.add(0,"3");
public void add(int index, E element) {//插入元素的方法
        checkPositionIndex(index);

        if (index == size)//如果当前插入的元素,是插入到双链表的末尾
            linkLast(element);
        else//如果当前插入的元素,不是插入到双链表的末尾
            linkBefore(element, node(index));
}

重点看一下linkLast和linkBefore

   /**
     * Links e as last element.
     */
    void linkLast(E e) {//如果插入的位置是双链表的末尾
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);//把元素封装成链表中的一个Node节点对象
        last = newNode;//把新节点赋值给last作为最后一个节点
        if (l == null)//如果双链表没插入该元素之前的最后一个节点为null,那么插入后,这个新节点就是第一个节点
            first = newNode;
        else//如果双链表没插入该元素之前最后一个节点不为null,那么之前的末尾节点的next插入的新节点
            l.next = newNode;
        size++;
        modCount++;
    }
    /**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {//如果插入的位置不是双链表的尾部
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);//封装新的Node节点对象
        succ.prev = newNode;//把新节点赋值给当前节点的前驱节点
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

由此可以看出,在LinkedList进行添加元素时,只是双链表的节点的前驱和后继指向做出了变化,没有过度消耗性能耗时的地方。

在这里需要注意的一点:双向链表查找index位置的节点时,有一个加速动作若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找

二、再看ArrayList:

ArrayList<String> arrayList=new ArrayList<>();
arrayList.add(0,"2");
public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

在这里我们重点关注System.arraycopy这个方法以及ensureCapacityInternal(size + 1)。

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }


private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

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

ensureCapacityInternal的作用是“确认ArrayList的容量,若容量不够,则增加容量。”在扩容的时候,用到了Arrays.copyOf方法,数组的拷贝。这个是一个比较耗时的操作。

真正耗时的操作是 System.arraycopy(elementData, index, elementData, index + 1, size - index);

Sun JDK包的java/lang/System.java中的arraycopy()声明如下:

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

arraycopy()是个JNI函数,它是在JVM中实现的。sunJDK中看不到源码。
实际上,我们只需要了解: System.arraycopy(elementData, index, elementData, index + 1, size - index); 会移动index之后所有元素即可这就意味着,ArrayList的add(int index, E element)函数,会引起index之后所有元素的改变!

数组复制,我们看到了两个方法,Arrays.copyOf和System.arraycopy。下面我们先分析下这俩方法:

(1)System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length):

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

src - 源数组。
srcPos - 源数组中的起始位置。
dest - 目标数组。
destPos - 目标数据中的起始位置。
length - 要复制的数组元素的数量。
该方法是用了native关键字,调用的为C++编写的底层函数,可见其为JDK中的底层函数。

(2)再来看看Arrays.copyOf();该方法对于不同的数据类型都有相应的方法重载:

//复杂数据类型
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

original - 要复制的数组
newLength - 要返回的副本的长度
newType - 要返回的副本的类型

//基本数据类型(其他类似byte,short···)
public static int[] copyOf(int[] original, int newLength) {
        int[] copy = new int[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

观察其源代码发现copyOf(),在其内部创建了一个新的数组,然后调用arrayCopy()向其复制内容,返回出去。
总结:
1.copyOf()的实现是用的是arrayCopy();
2.arrayCopy()需要目标数组,对两个数组的内容进行可能不完全的合并操作。
3.copyOf()在内部新建一个数组,调用arrayCopy()将original内容复制到copy中去,并且长度为newLength。返回copy;

分析2:为什么LinkedList中随机访问很慢,而ArrayList中随机访问很快

(1)我们先看LinkedList:

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

 

通过get(int index)获取LinkedList第index个元素时先是在双向链表中找到要index位置的元素;找到之后再返回。
双向链表查找index位置的节点时,有一个加速动作若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。

(2)再看ArrayList:

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

        return (E) elementData[index];
    }

 

因为ArrayList底层是一个数组,ArrayList取元素也就是通过索引从数组中取元素。通过get(int index)获取ArrayList第index个元素时。直接返回数组中index位置的元素,而不需要像LinkedList一样进行查找。

总结

由上面的分析可知,LinkedList在元素的插入和删除时,比ArrayList的插入删除效率更高。而在查找元素时,ArrayList比LinkedList效率高。


 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值