Java List分析

作为Java中重要的一种数据结构,有必要好好研究下

List接口三个主要实现类ArrayList, Vector和LinkedList,下面从底层维护,增加,查找和删除效率分析ArrayList和LinkedList


ArrayList

通过查看源代码可知,ArrayList维护着Object[] elementData数组和int size变量(内部数组中的元素个数,初始值为0)

1. 下面看看add(E e)方法实现:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

从源代码可知,在每次调用add(E e)方法将元素增加到队列尾端,在增加之前都需要进行容量检查,即确保内部数组有足够的空间,进入到ensureCapacityInternal(int minCapacity)方法中

private void ensureCapacityInternal(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

在该方法中,先做if判断,如果此时 我们需要的最小容量(minCapacity)大于当前内部数组的长度(elementData.length),那么内部数组容量不够,需要进行扩容,随后调用grow(int minCapacity)方法;否则返回add(E e)方法,直接将该元素加入到内部数组中,size++(表示此时内部数组中的元素个数)。

下面看下gorw(int 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);
    }
int newCapacity = oldCapacity + (oldCapacity >> 1);
这条语句表示扩容为原来的1.5倍。然后进行数组复制
elementData = Arrays.copyOf(elementData, newCapacity);
最终调用System.arraycopy()方法。

由以上可知,在调用ArrayList的add(E e)方法,即将元素插入到数组尾部,故时间复杂度为O(1)。


下面看看LinkedList的add(E e)方法实现

LinkedList底层维护着一个双向链表数据结构,一个表项结点包含3个部分:元素内容,前驱指针和后驱后驱。在JDK的实现中,无论LinkedList是否为空,链表内都有一个header结点--即头结点。

通过源代码可以看到其结点的定义

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(E e)方法实现

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

add(E e)方法也是将元素插入到链表的尾部,但是与ArrayList的add()方法不同的是,该方法没有做容量检查。是直接生成一个Node<E>结点并将其插入到链表尾部,其时间复杂度也为O(1)。


下面通过实验比较着两种方法的速度

Object obj = new Object();
long begin = System.currentTimeMillis();
for(int i = 0; i < 500000; i++)
{
	list.add(obj);
}
		
System.out.println(System.currentTimeMillis() - begin);
通过输出,ArrayList耗时15ms, LinkedList耗时31ms.因为LinkedList在添加元素时需要不断的生成新的对象;而对于ArrayList,只有在空间不足时才产生数组扩容和数组复制,所以ArrayList的add()方法效率还是非常高的。


2. 增加元素到列表任意位置

void add(int index, E element)

由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该元素后的所有元素需要重新排列,所以其效率相对会非常低,下面看看该方法源代码:

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

该方法也是要做容量检查,然后做数据复制。大量的数组重组操作会导致系统性能低下。并且,插入的元素在List的位置越靠前,数组重组的开销也越大。尽可能的将元素插入到List的尾端,有助于提高该方法的性能。

该方法平均移动元素的次数为(n - 1)/2,故时间复杂度为O(n).


LinkedList的void add(int index, E element)方法

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

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

对于LinkedList来说,在List的任何位置插入数据都是一样的,不会因为插入的位置而影响到该方法的性能。

该方法时间复杂度为O(1).


下面每次都将元素插入到list的最前端,比较着两个方法的差别

Object obj = new Object();
for(int i = 0; i < 50000; i++)
{
	list.add(0, obj);
}
结果显示ArrayList耗时827ms, 而LinkedList耗时31ms。可见两者在性能上的差异。


3.E get(int index)方法比较

ArrayList该方法源码

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

        return elementData(index);
    }

直接返回该对象。故时间复杂度为O(1).


LinkedList中该方法源码

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

/**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }

由源码可知,LinkedList的get(int index)方法的实现是,若该元素在链表的前半段,则从前半段找,否则从后半段找。但是如果我们要查找中间的那个元素,那几乎要遍历完半个List,当List拥有大量元素时,其效率是很低的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值