ArrayList这是LinkedList最后的倔强了

本文详细分析了LinkedList和ArrayList在插入、删除元素时的效率差异,揭示了头部、中部和尾部操作的性能对比,并根据数据量大小和操作类型给出了实际应用场景的建议。

在面试的时候经常被问到LinkedList和ArrayList有什么区别?
正常的回答是:ArrayList是基于数组实现的, LinkedList是基于链表实现的。

好一点的如下面的回答:

  1. LinkedList 在插入和删除数据时效率更高;
  2. ArrayList 在查找某个 index 的数据时效率更高;
  3. LinkedList 实现了 List 和 Deque 接口,一般称为双向链表;
  4. ArrayList 实现了List 接口,称为动态数组;
  5. LinkedList 比 ArrayList 需要更多的内存;

接着面试官问:它们都在什么场景下使用吗?

答:因为ArrayList是基于数组实现的,所以在遍历的时候,ArrayList的效率是要比LinkedList高的, LinkedList是基于链表实现的,所以在进行新增/删除元素的时候, LinkedList的效率是要比ArrayList 高的。

是这样么,没错,我以前也是这么说的,那时候天真的以为我回答得非常的好,好尴尬哦

但是LinkedList在插入和删除数据时效率更高,真的是这样吗?聪明的孩子都知道这么说就不是了。

当然不是,显然是在一定的条件下才成立,为了验证这一点,接下来我们根据测试以及源码分析一下这个问题。

插入元素谁快谁慢

ArrayList源码:

ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。

添加到数组末尾的源码:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1); // Increments modCount!!  
        elementData[size++] = e;// 向末尾追加一个元素,显然时间复杂度为O(1)
        return true;
    }

插入到指定位置的源码:

    public void add(int index, E element) {
        rangeCheckForAdd(index); //1 先检查插入的位置是否在合理的范围之内

        ensureCapacityInternal(size + 1);  // Increments modCount!!//2 然后判断是否需要扩容
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);//3 再把该位置以后的元素复制到新添加元素的位置之后
        elementData[index] = element;//4 最后通过索引将元素添加到指定的位置
        size++;
    }

LinkedList源码:

LinkedList新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。

添加到队尾的源码:

public boolean add(E e) {
    linkLast(e); // 向末尾追加元素
    return true;
}

void linkLast(E e) {
    final Node<E> l = last; //先将队尾的节点 last 存放到临时变量 l 中
    final Node<E> newNode = new Node<>(l, e, null); 
    last = newNode; //然后生成新的 Node 节点,并赋给 last
    if (l == null)//如果 l 为 null,说明是第一次添加
        first = newNode; //所以 first 为新的节点
    else
        l.next = newNode; //否则将新的节点赋给之前last的next。
    size++;
    modCount++;
}

添加到指定位置的源码:

    public void add(int index, E element) {
        checkPositionIndex(index);//先检查插入的位置是否在合理的范围之内
       //判断插入的位置是否是队尾,如果是,添加到队尾;否则执行linkBefore()方法。
        if (index == size)
            linkLast(element);
        else
        //linkBefore方法里面还有一个node方法:
            linkBefore(element, node(index));
    }
    /**
     * Returns the (non-null) Node at the specified element index.
     */
      //查找指定位置上的元素,这一步是需要遍历LinkedList
    Node<E> node(int index) {
        // assert isElementIndex(index);
        // 如果index小于size/2,就从first开始遍历,即从头往后找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {// 如果index大于等于size/2,就从last开始遍历,即从尾向前找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
        //从上面可以看出如果插入的位置越靠近LinkedList的中间位置,遍历所花费的时间就越多。
    }
        /**
     * Inserts element e before non-null Node succ.
     */
     //找到指定位置上的元素succ之后,就开始执行linkBefore()方法了
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev; //先将succ的前一个节点prev存放到临时变量pred中
        final Node<E> newNode = new Node<>(pred, e, succ);//生成新的Node节点(newNode)
        //将succ的前一个节点变更为newNode
        succ.prev = newNode;
        //如果pred为null,说明插入的是队头,所以first为新节点,否则将pred的后一个节点变更为 newNode
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

从头部开始插入元素:

	public static void main(String[] args) throws ParseException {
		
		List<Integer> list = new ArrayList<Integer>();
		LinkedList<Integer> linkedList = new LinkedList<Integer>();
		long start = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < 50000; ++i) {
            list.add(0,1);
        }
        long end = System.currentTimeMillis(); // 结束时间
		
		System.out.println("ArrayList添加数据时间: "+(end-start)+"ms");
		
		long start2 = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < 50000; ++i) {
        	linkedList.add(0, i); 
        }
        long end2 = System.currentTimeMillis(); // 结束时间
		
		System.out.println("LinkedList添加数据时间: "+(end2-start2)+"ms");
	}
ArrayList添加数据时间: 329ms
LinkedList添加数据时间: 5ms

从时间消耗看到ArrayList比LinkedList要花费的时间多,从源码中ArrayList要对头部以后的元素进行复制的操作。

从中间开始插入元素:

	public static void main(String[] args) throws ParseException {
		
		List<Integer> list = new ArrayList<Integer>();
		LinkedList<Integer> linkedList = new LinkedList<Integer>();
		long start = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < 100000; ++i) {
            list.add(list.size()/5,1);
        }
        long end = System.currentTimeMillis(); // 结束时间
		
		System.out.println("ArrayList添加数据时间: "+(end-start)+"ms");
		
		long start2 = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < 100000; ++i) {
        	linkedList.add(linkedList.size()/5, i); 
        }
        long end2 = System.currentTimeMillis(); // 结束时间
		
		System.out.println("LinkedList添加数据时间: "+(end2-start2)+"ms");
	}
ArrayList添加数据时间: 962ms
LinkedList添加数据时间: 1622ms

从时间消耗看到ArrayList比LinkedList要花费的时间少,从源码中LinkedList需要进行遍历的操作。

从尾部开始插入元素:

	public static void main(String[] args) throws ParseException {
		
		List<Integer> list = new ArrayList<Integer>();
		LinkedList<Integer> linkedList = new LinkedList<Integer>();
		long start = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < 1000000; ++i) {
            list.add(i);
        }
        long end = System.currentTimeMillis(); // 结束时间
		
		System.out.println("ArrayList添加数据时间: "+(end-start)+"ms");
		
		long start2 = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < 1000000; ++i) {
        	linkedList.add(i); 
        }
        long end2 = System.currentTimeMillis(); // 结束时间
		
		System.out.println("LinkedList添加数据时间: "+(end2-start2)+"ms");
	}
	
ArrayList添加数据时间: 17ms
LinkedList添加数据时间: 22ms

从时间消耗看到ArrayList比LinkedList要花费的时间少,数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列,ArrayList添加元素需要进行数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。

删除元素谁快谁慢

ArrayList源码:

ArrayList删除元素有两种方式:
remove(Object o):根据对象删除元素。
remove(int index):根据索引删除元素。

    public E remove(int index) {
    	// 首先检查下标是否越界,如果下标越界,将抛出异常
        rangeCheck(index);
        // 当前列表的修改次数加1
		// 这个参数是用于迭代器fail-fast机制的
		// 当在遍历列表时,如果对列表进行删除操作时,将会抛出异常
        modCount++;]
        // 根据下标获取对应的值
        E oldValue = elementData(index);
        // 计算要删除位置的元素后还有几个元素,用于后面的操作
        int numMoved = size - index - 1;
        // 如果要删除的元素不是最后一个元素
        if (numMoved > 0)
        	// 这是jdk的一个本地方法,用于将一个数组从指定位置复制到目标数组的指定位置
        	// 其中numMoved就是要复制的个数,也就是被删除元素后面的元素个数
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
         // 把列表大小减1,并把最后一个元素置空,让垃圾收集器把它回收
         // 这里如果不置空,它将会保存着一个引用,那么垃圾收集器将无法回收它,可能会造成内存泄漏
        elementData[--size] = null; // clear to let GC do its work
        //将被删除的值返回
        return oldValue;
    }
    public boolean remove(Object o) {
    // 如果对象为空(ArrayList允许元素为空)
        if (o == null) {
        //循环查找ArrayList中是否有null元素,如果有则返回true
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {//如果指定元素不为空,则循环
            for (int index = 0; index < size; index++)
            //将指定元素与ArrayList中的元素进行equals比较
                if (o.equals(elementData[index])) {
                //如果相等,则调用fastRemove移除并返回true;
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

这里重点是fastRemove这个方法,这个是ArrayList的私有移除方法,方法源码:

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
    }

fastRemove()方法跳过边界检查并且不会返回值,因为这里的index是根据元素得出的,所以不可能越界,因为上一级返回了是否成功的标志,这里也不需要在返回值操作。

remove方法可以指定索引移除值,也可以按指定对象进行移除。移除某个值后,其后所有的值都会往前移动一位。

LinkedList源码:

LinkedList删除元素有3种方式
remove():无参方法会直接删除链表的第一个节点
remove(Object o):删除传入的对象
remove(int index):删除第index个链表节点

remove()源码:

//删除第一个节点
   public E remove() {
        return removeFirst();
    }
    public E removeFirst() {
    //first指向链表中的第一个结点
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
            //主要方法是这个
        return unlinkFirst(f);
    }
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        //获取当前节点的元素
        final E element = f.item;
        //将next引用指向下一个节点
        final Node<E> next = f.next;
        
        //将删除的元素置空,垃圾回收器会自动清除内容
        f.item = null;
         //将删除的内容
        f.next = null; // help GC
         //因为删除当前节点元素(第一个节点),所以此处将下一个节点赋值给第一个
        first = next;
         //判断如果没有下一个节点,那么最后一个节点就不存在null
        if (next == null)
            last = null;
        else
         //此处的next.prev即第一个结点,要删除,所以赋值为null
            next.prev = null;
            //链表大小减一
        size--;
        //修改记录加一
        modCount++;
        //将要删除的值返回
        return element;
    }

删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可

c removeLast:删除最后一个节点,删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点就行。

remove(Object o)源码:

 public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
            //当有节点的值为null时
                if (x.item == null) {
                //调用unlink方法删除节点
                    unlink(x);
                    return true;
                }
            }
        } else {//如果传入对象不为null同样遍历全表
            for (Node<E> x = first; x != null; x = x.next) {
            //调用对象的equals方法判断有没有与传入对象相等的节点
                if (o.equals(x.item)) {          
                //如果有调用unlink方法删除节点                                                                                                               
                    unlink(x);
                    return true;
                }
            }
        }
        //其他情况返回false
        return false;
    }

remove(int index)源码:

public E remove(int index) {
       //判断传入第index个节点是否存在
        checkElementIndex(index);
        //如果存在调用node(index) 方法(前后半段遍历,和新增元素操作一样)找到节点Node,然后调用unlink(Node)解除节点的前后引用,同时更新前节点的后引用和后节点的前引用:
        return unlink(node(index));
    }

上面调用unlink()方法的 步骤解析:

E unlink(Node<E> x) {
        // assert x != null;
        //创建三个临时变量存储传入节点的信息
        //element 存储节点的对应对象
        //node<E> next 存储下一个节点的地址
        //node<E> prev 存储上一个节点的地址
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
       //如果上一个节点为null(prev=null)说明传入节点为头节点
        if (prev == null) {
        //此时将下一个节点的地址赋值给first
            first = next;
        } else {
        //其他则将上一个节点的next指向传入节点的下一个节点
            prev.next = next;
            //传入节点的prev赋值为null
            x.prev = null;
        }
//如果传入节点之后没有节点(next=null)说明传入节点为尾节点
        if (next == null) {
        //此时上一个节点赋值给尾节点
            last = prev;
        } else {
        //其他则将上一个节点的地址赋值给下一个节点的prev
            next.prev = prev;
           // 将传入节点的next赋值为null
            x.next = null;
        }
       //最后将传入节点的值赋值为null
        x.item = null;
        //链表中的节点个数减1
        size--;
        //修改记录加一
        modCount++;
        //将要删除的值返回
        return element;
    }

LinkedList在删除比较靠前和比较靠后的元素时非常高效,但如果删除的是中间位置的元素,效率就比较低。

从头部开始删除元素:

public static void main(String[] args) throws ParseException {
		
		     LinkedList<String> linkedList = new LinkedList<String>();
		     ArrayList<String> arrayList = new ArrayList<String>();
	        for (int i=0;i<50000;i++) {
	        	arrayList.add(i + "最帅的人");
	        	linkedList.add(i + "最帅的人");
	        }
	        long timeStart = System.currentTimeMillis();

	        for (int i=0;i<50000;i++) {
	        	linkedList.removeFirst();
	 
	        }

	        long timeEnd = System.currentTimeMillis();

	        System.out.println("LinkedList删除花费的时间 " + (timeEnd - timeStart));
	        
	        timeStart = System.currentTimeMillis();
	        for (int i=0;i<50000;i++) {
	        	arrayList.remove(0);
	 
	        }
	        timeEnd = System.currentTimeMillis();
	        System.out.println("ArrayList删除花费的时间 " + (timeEnd - timeStart));
	        
		
	}
LinkedList 从集合头部位置删除元素花费的时间 4
ArrayList 从集合头部位置删除元素花费的时间 430

从集合头部删除元素时,ArrayList花费的时间比LinkedList要多很多。

从中间开始删除元素:

	public static void main(String[] args) throws ParseException {
		
		     LinkedList<String> linkedList = new LinkedList<String>();
		     ArrayList<String> arrayList = new ArrayList<String>();
	        for (int i=0;i<50000;i++) {
	        	arrayList.add(i + "最帅的人");
	        	linkedList.add(i + "最帅的人");
	        }
	        long timeStart = System.currentTimeMillis();

	        for (int i=0;i<50000;i++) {
	        	linkedList.remove(linkedList.size()/2);
	 
	        }

	        long timeEnd = System.currentTimeMillis();

	        System.out.println("LinkedList删除花费的时间 " + (timeEnd - timeStart));
	        
	        timeStart = System.currentTimeMillis();
	        for (int i=0;i<50000;i++) {
	        	arrayList.remove(arrayList.size()/2);
	 
	        }
	        timeEnd = System.currentTimeMillis();
	        System.out.println("ArrayList删除花费的时间 " + (timeEnd - timeStart));
	        
		
	}
LinkedList删除花费的时间 1623
ArrayList删除花费的时间 130

从集合中间位置删除元素时,ArrayList花费的时间要比LinkedList少很多。

从尾部开始删除元素:

	public static void main(String[] args) throws ParseException {
		
		     LinkedList<String> linkedList = new LinkedList<String>();
		     ArrayList<String> arrayList = new ArrayList<String>();
	        for (int i=0;i<50000;i++) {
	        	arrayList.add(i + "最帅的人");
	        	linkedList.add(i + "最帅的人");
	        }
	        long timeStart = System.currentTimeMillis();

	        for (int i=0;i<50000;i++) {
	        	linkedList.removeLast();
	 
	        }

	        long timeEnd = System.currentTimeMillis();

	        System.out.println("LinkedList删除花费的时间 " + (timeEnd - timeStart));
	        
	        timeStart = System.currentTimeMillis();
	        for (int i=0;i<50000;i++) {
	        	arrayList.remove(arrayList.size()-1);
	 
	        }
	        timeEnd = System.currentTimeMillis();
	        System.out.println("ArrayList删除花费的时间 " + (timeEnd - timeStart));
	        
		
	}
LinkedList删除花费的时间 7
ArrayList删除花费的时间 3

从集合尾部删除元素时,ArrayList花费的时间要比LinkedList少一点。

总结

在集合里面插入元素速度比对结果:

  • 头部插入:LinkedList 快
  • 中间、尾部插入:ArrayList 快
在集合里面删除元素速度比对结果:
  • 头部删除:LinkedList 快
  • 中间、尾部插入:ArrayList快

数据量大的话使用ArrayList,查询速度快,而且插入和删除效率也比较高,数据量小主要进行插入、删除的建议使用LinkedList。

因此,面试的时候如果面试官问到可以把上面所说的讲一遍,基本没有多大的问题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值