java中ArrayList与LinkedList的区别

一、背景

面试题中经常会被面试官问到ArrayList和LinkedList的区别,下面从源码角度来对他们进行一下简单的阐述,相信会对它们有一个更全面深入的了解。

首先,ArrayList和LinkedList都实现了List接口,ArrayList的底层是通过【动态数组】实现的,LinkedList底层是通过【链表】实现的。

二、ArrayList

1、通过add(e)方法添加元素
java中的数组一旦定义之后长度length就不可变了,是不可变数组;而python是可变数组,这点需要注意这两种语言的不同;ArrayList可以不断的通过add添加元素,它的size也是变化的,数组的长度又是不可变的,而ArrayList的底层是数组,它们不就矛盾了吗?别急,ArrayList是通过判断当前集合中元素的size数与数组的长度比较,如果size>数组length,再对数组扩容,然后将元素赋值给扩容后的数组
下面是截取的ArrayList类中的关于add方法的代码

private static final int DEFAULT_CAPACITY = 10;  //定义一个int常量,值为10
private static final Object[] EMPTY_ELEMENTDATA = {}; //定义一个空数组常量,值为{}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//定义一个默认数组常量,值为{}
transient Object[] elementData; //定义一个数组
private int size;  //定义size

/**
* 无参构造方法,new一个ArrayList对象后,实际上也创建并初始化了一个elementData的数组,且
* 这个数组为空数组{},length长度为0
*/
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* 执行add(e)方法添加元素,可以看到先调用ensureCapacityInternal方法,再对
* elementData数组进行赋值。
*/
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

/**
* ensureCapacityInternal方法传入的minCapacity形参对应的实参值=size+1,它下面再去调用其他
* 方法,我们一层层深入,抽丝剥茧到后面的方法
*/
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// calculateCapacity方法根据传入的数组是否为空和minCapacity参数来确认并返回int
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // arrayList第一次添加元素e时,size=0,minCapacity=1,调用此代码,return返回10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // arrayList 第二次及以后添加元素e,会直接执行此处代码,并return (size+1)>=2
    return minCapacity;
}

/**
* 上面calculateCapacity方法返回值作为实参传递给下面的ensureExplicitCapacity方法的形参并进* 行判断处理。
*/
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    /**
    * 第1种情况:第一次添加元素e时,minCapacity=10,数组长度length=0
	* 第2种情况:第二次及以后添加e,minCapacity=size+1>=2
	* minCapacity与数组elementData长度进行比较,如果前者大于数组长度,则进行数组扩容,执行
	* grow方法。
	* 当属于第1种情况时,数组长度=0,我们无法添加元素到数组中,所以需要执行grow()方法扩容,扩容
	* 的本质就是执行Arrays.copyOf()方法,得到长度为10的数组,然后再给数组赋值;
	* 当属于第2种情况时,数组长度已经进行了第一次扩容,length=10,当添加第二个元素e时,
	* minCapacity=2 < elementData.length=10,也就是说数组容量足够,可以直接添加元素,不必
	* 扩容
    */
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 下面的grow方法就是扩容的核心代码
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);
}

总结一下:通过add(e)方法在集合尾部添加数据,效率还是比较高的,因为不涉及数组元素的复制移动,但有时涉及到扩容
2、通过get(index)根据索引获取元素对象
下面是ArrayList类中的关于get(index)方法的代码

public E get(int index) {
		// 校验索引index是否越界
        rangeCheck(index);
        // 调用elementData方法
        return elementData(index);
    }

// 返回index下标对应的数组元素,不需要遍历
E elementData(int index) {
        return (E) elementData[index];
    }

总结一下:get(index)不需要遍历,直接取出相应下标的数组元素,效率较高
3、通过add(index,e)指定位置添加元素
下面是ArrayList类中的关于add(index,e)方法的源代码

public void add(int index, E element) {
		// 检验index有效性
        rangeCheckForAdd(index);
        // 根据传参size+1,判断是否扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 数组复制,指定index对应的位置空出
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 数组index下标赋值插入的数据
        elementData[index] = element;
        size++;
    }

原理图如下:
在这里插入图片描述
总结一下:add(index,e)方法涉及到元素的整体复制向后移动,元素下标也会发生变化,此种方式添加元素效率较低,数组容量不足,也会进行扩容

4、remove(index)删除元素源码

public E remove(int index) {
		// 检查下标的有效性
        rangeCheck(index);
        modCount++;
        // 获取被删除元素value值
        E oldValue = elementData(index);
		// 被删元素后面需要被移动的元素个数
        int numMoved = size - index - 1;
        if (numMoved > 0)
        	// 数组复制并向前移动1位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

总结一下:remove(index)删除元素,会导致剩下的元素整体复制移动,元素下标会发生变化,因此该方式删除元素数据效率较低

二、LinkedList

1、通过add(e)方法添加元素
以下是LinkedList类中部分源码。

	......省略......

	// 声明容量size
	transient int size = 0;
	
	// 声明首节点
    transient Node<E> first;

	// 声明尾节点
    transient Node<E> last;

	// 无参构造方法,执行后size=0;first=null;last=null
    public LinkedList() {
    }
    
    // add方法添加元素
	public boolean add(E e) {
		// 调用的核心方法
        linkLast(e);
        return true;
    }
    
	void linkLast(E e) {
		// 添加元素之前,先将当前LinkedList对象的尾部节点赋给l对象
        final Node<E> l = last;
        // 创建一个新Node对象:上一个节点指向l,下一个节点指向null,元素E对象为e
        final Node<E> newNode = new Node<>(l, e, null);
        // 将newNode新节点赋给last对象
        last = newNode;
        // 如果当前LinkedList对象没有尾节点,即l==null,说明LinkedList对象中没有节点元素。
        if (l == null)
        	// 这种情况下,将新Node节点赋给首节点
            first = newNode;
        else
        	// 如果LinkedList对象中已经存在尾节点,则将该尾节点的下一个节点指向新添加的节点
            l.next = newNode;
        // size数加1
        size++;
        modCount++;
    }

	// 定义私有内部静态Node类:Node对象有三个属性->元素E、指向上一个节点prev、指向下一个节点next
	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对象结构示意图:
在这里插入图片描述

总结一下:LinkedList对象中通过add()方法添加元素时,对已经存在的元素没有影响,没有对其他元素的复制移动等操作,效率高

2、通过add(index,e)添加元素到指定位置
以下是相关的源码,关键代码做了注释。

	public void add(int index, E element) {
		// 校验index是否越界
        checkPositionIndex(index);
		// index == size时,直接在最后添加,方法同上add(e)
        if (index == size)
            linkLast(element);
        else
        	// 在集合首尾之间指定index处添加元素场景下的核心方法
            linkBefore(element, node(index));
    }
    
    // add(index,e)的底层核心方法
    void linkBefore(E e, Node<E> succ) {
        // 获取指定index处Node对象的上一个节点对象pred
        final Node<E> pred = succ.prev;
        // new一个新节点对象,指定它的上一个节点对象是pred,下一个节点对象是succ
        final Node<E> newNode = new Node<>(pred, e, succ);
        // 重新设置指定index索引Node对象的上一个节点对象为newNode
        succ.prev = newNode;
        // 判断指定index处Node对象的上一个节点对象pred是否为空
        if (pred == null)
        	// 如果为空,则指定newNode节点为首节点
            first = newNode;
        else
        	// 如果不为空,则将指定indexNode对象的上一个节点对象的pred的下一个节点对象设置为newNode
            pred.next = newNode;
        size++;
        modCount++;
    }

	// 该方法返回指定index处的Node节点对象
	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;
        }
    }

总结一下:add(index,e)方法添加元素到集合中的指定位置,只是改变了上一个节点和下一个节点的指向位置,其他元素不受影响,所以比ArrayList的add(index,e)的效率要高,但需注意在查找index节点时,进行了遍历,如果size比较大的话,遍历会比较耗时

3、通过remove(index)的方法删除元素。
以下是LinkedList中相关源码,关键代码做了注释

	public E remove(int index) {
		// 检查index索引是否合法
        checkElementIndex(index);
        // 调用node和unlink方法
        return unlink(node(index));
    }

	/**
     * Returns the (non-null) Node at the specified element index.
     * 返回指定index索引处的Node<E>对象
     */
    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;
        }
    }

	E unlink(Node<E> x) {
        // 获取index处Node对象的元素e
        final E element = x.item;
        // 获取index处Node对象的下一个节点Node对象next
        final Node<E> next = x.next;
        // 获取index处Node对象的上一个节点Node对象prev
        final Node<E> prev = x.prev;
		// 若index节点指向的上一个节点为null,说明index是首节点,删除之后,将next节点赋值给首节点
        if (prev == null) {
            first = next;
        } else {
        	// 若index节点指向的上一个节点prev不为null,说明被删节点不是首节点,此时将prev的下一个节点指向next节点
            prev.next = next;
            // index节点指向的上一个节点赋null值,代表取消它的指向关系。
            x.prev = null;
        }
		// 若index节点指向的下一个节点next为null,说明被删节点是尾节点,将prev节点赋值给尾节点
        if (next == null) {
            last = prev;
        } else {
        	// 若next不为null,说明被删节点不是尾节点,此时将next节点的上一个节点指向prev节点
            next.prev = prev;
            // index节点指向的下一个节点赋值为null值,代表取消它的指向关系
            x.next = null;
        }
		// index节点对应的元素Element赋值为null,结合上面的x.prev=null/x.next=null,代表该节点已经被删除了。
        x.item = null;
        size--;
        modCount++;
        return element;
    }

总结一下:remove(index)方法删除集合中的元素只是改变了上一个节点和下一个节点的指向位置,对其他元素没有造成影响,效率比较高,但需注意在查找index节点时,进行了遍历,如果size比较大的话,遍历会比较耗时

4、通过get(index)获取集合中的元素
以下是LinkedList中的部分源码,关键部分做了注释。

	public E get(int index) {
		// 检查index索引合法性,是否越界。
        checkElementIndex(index);
        // 调用node方法
        return node(index).item;
    }

	/**
     * Returns the (non-null) Node at the specified element index.
     * 此方法根据传入的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(index)方法通过遍历查询元素,效率比较低;而ArrayList中的get(index)直接通过下标获取数组元素,不用遍历,效率更高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值