上次被 ArrayList 锤了一拳后,LinkedList 很不服气,做出最后一击(3)

简介: 上次被 ArrayList 锤了一拳后,LinkedList 很不服气,做出最后一击

经过源码分析以后,小伙伴们是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!”


当两者的起始长度是一样的情况下:


如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。

public class ArrayListTest {
    public static void addFromHeaderTest(int num) {
        ArrayList<String> list = new ArrayList<String>(num);
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            list.add(0, i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();
        System.out.println("ArrayList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}
/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class LinkedListTest {
    public static void addFromHeaderTest(int num) {
        LinkedList<String> list = new LinkedList<String>();
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            list.addFirst(i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();
        System.out.println("LinkedList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}



num 为 10000,代码实测后的时间如下所示:


ArrayList从集合头部位置新增元素花费的时间595

LinkedList从集合头部位置新增元素花费的时间15



ArrayList 花费的时间比 LinkedList 要多很多。


如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。

public class ArrayListTest {
    public static void addFromMidTest(int num) {
        ArrayList<String> list = new ArrayList<String>(num);
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            int temp = list.size();
            list.add(temp / 2 + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();
        System.out.println("ArrayList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}
public class LinkedListTest {
    public static void addFromMidTest(int num) {
        LinkedList<String> list = new LinkedList<String>();
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            int temp = list.size();
            list.add(temp / 2, i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();
        System.out.println("LinkedList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}



num 为 10000,代码实测后的时间如下所示:


ArrayList从集合中间位置新增元素花费的时间1

LinkedList从集合中间位置新增元素花费的时间101



ArrayList 花费的时间比 LinkedList 要少很多很多。


如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。

public class ArrayListTest {
    public static void addFromTailTest(int num) {
        ArrayList<String> list = new ArrayList<String>(num);
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            list.add(i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();
        System.out.println("ArrayList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}
public class LinkedListTest {
    public static void addFromTailTest(int num) {
        LinkedList<String> list = new LinkedList<String>();
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            list.add(i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();
        System.out.println("LinkedList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}


num 为 10000,代码实测后的时间如下所示:


ArrayList从集合尾部位置新增元素花费的时间69

LinkedList从集合尾部位置新增元素花费的时间193

1

2

ArrayList 花费的时间比 LinkedList 要少一些。


这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。


当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。


04、ArrayList 和 LinkedList 删除元素时究竟谁快?


1)ArrayList


ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(remove(Object)),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(remove(int))。


public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}
public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;
    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);
    return oldValue;
}



但从本质上讲,都是一样的,因为它们最后调用的都是 fastRemove(Object, int) 方法。


private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}


从源码可以看得出,只要删除的不是最后一个元素,都需要数组重组。删除的元素位置越靠前,代价就越大。


2)LinkedList


LinkedList 删除元素的时候,有四种常用的方式:


remove(int),删除指定位置上的元素

public E remove(int index) {

   checkElementIndex(index);

   return unlink(node(index));

}



先检查索引,再调用 node(int) 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 unlink(Node) 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用:


 

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        x.item = null;
        size--;
        modCount++;
        return element;
    }
remove(Object),直接删除元素
public boolean remove(Object o) {
    if (o == null) {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
也是先前后半段遍历,找到要删除的元素后调用 unlink(Node)。
removeFirst(),删除第一个节点
public E removeFirst() {
    final LinkedList.Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
private E unlinkFirst(LinkedList.Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final LinkedList.Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

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


removeLast(),删除最后一个节点

删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。


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


这里就不再做代码测试了,感兴趣的小伙伴可以自己试试,结果和新增元素保持一致:


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


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


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


我本地的统计结果如下所示,小伙伴们可以作为参考:


ArrayList从集合头部位置删除元素花费的时间380

LinkedList从集合头部位置删除元素花费的时间4

ArrayList从集合中间位置删除元素花费的时间381

LinkedList从集合中间位置删除元素花费的时间5922

ArrayList从集合尾部位置删除元素花费的时间8

LinkedList从集合尾部位置删除元素花费的时间12


05、ArrayList 和 LinkedList 遍历元素时究竟谁快?


1)ArrayList


遍历 ArrayList 找到某个元素的话,通常有两种形式:


get(int),根据索引找元素
public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}


由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。


indexOf(Object),根据元素找索引
public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}
int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    return -1;
}


根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。


相关文章
|
存储 算法 Java
Arrays:点燃你的数组操作技巧的隐秘武器。
Arrays 是我们在处理数组时的一把利器。它提供了丰富的方法和功能,使得数组操作变得更加简单、高效和可靠。无论是排序、搜索、比较还是复制,Arrays 都能够满足我们的需求。
Arrays:点燃你的数组操作技巧的隐秘武器。
|
Java API
公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!(2)
公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!
194 0
公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!(2)
|
Java 编译器
公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!(1)
公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!
147 0
公司新来一个同事:为什么 HashMap 不能一边遍历一边删除?一下子把我问懵了!(1)
|
存储 安全 算法
《我要进大厂》- Java集合夺命连环13问,你能坚持到第几问?(Map | Collections)
《我要进大厂》- Java集合夺命连环13问,你能坚持到第几问?(Map | Collections)
《我要进大厂》- Java集合夺命连环13问,你能坚持到第几问?(Map | Collections)
|
存储 小程序 Java
Java数据结构之基于ArrayList编写大众麻将和扑克牌洗牌小练习
本文讲解:Java数据结构之基于ArrayList编写大众麻将和扑克牌洗牌小练习
|
存储
值得思索的:ArrayList和线性表,你确定错过这次机会
值得思索的:ArrayList和线性表,你确定错过这次机会
46 0
值得思索的:ArrayList和线性表,你确定错过这次机会
|
存储 安全 Java
java集合类史上最细讲解 - List篇
从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
117 0
java集合类史上最细讲解 - List篇
|
存储 算法 Java
java集合类史上最细讲解 - HashSet篇
1.Set接口方法 Set接口对象存放的数据是没有重复,且数据是无序存放的(添加顺序和存放顺序不一致,但是这个存放的顺序是固定的,不会随机变化)🤳 代码示例:
112 0
java集合类史上最细讲解 - HashSet篇
|
Java 程序员
java集合类史上最细讲解 - Map篇
1.Map接口介绍 Map用于保存具有映射关系的数据:Key - Value 对于Set,底层其实依然是一个Map,但是Set选择不使用Value,也就是Set的Value值始终是一个常量😁 Map中的Key和Value可以是任何类型的数据,会封装到HashMap$Node对象中 Map中的Key不能重复,但是Value可以重复,当有相同的Key时,等价与替换操作😀
121 0
java集合类史上最细讲解 - Map篇
|
Java 测试技术 API
满地坑!细数List的10个坑
今天我们主要来说一说List操作在实际使用中有哪些坑,以及面对这些坑的时候我们要怎么解决。