• JDK源码分析(4)之 LinkedList 相关


    LinkedList的源码大致分三个部分,双向循环链表的实现、List的API和Deque的API。

    一、定义

    public class LinkedList<E>
      extends AbstractSequentialList<E>
      implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    

    从类定义和图中也能很清晰的看到,LinkedList的结构大致分为三个部分;同时和ArrayList相比,他并没有实现RandomAccess接口,所以他并不支持随机访问操作;另外可以看到他的List接口是通过AbstractSequentialList实现的,同时还实现了多个迭代器,表明他的访问操作时通过迭代器完成的。

    二、链表结构

    常见链表

    LinkedList是基于双向循环链表实现的,所以如图所示,当对链表进行插入、删除等操作时,

    • 首先需要区分操作节点是否为首尾节点,并区分是否为空,
    • 然后再变更相应prenext的引用即可;
    void linkFirst(E e)
    void linkLast(E e)
    void linkBefore(E e, Node<E> succ)
    E unlinkFirst(Node<E> f)
    E unlinkLast(Node<E> l)
    E unlink(Node<E> x)
    
    /**
     * 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;
      }
    }
    

    上面所列的方法封装了对双向循环链表常用操作,其中node(int index)是随机查询方法,这里通过判断index是前半段还是后半段,来确定遍历的方向以增加效率。
    同时在LinkedList中有关ListDeque的API也是基于上面的封装的方法完成的。具体代码比较简单,就不挨着分析了。

    三、序列化和反序列化

    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
    

    可以看到LinkedList的成员变量都是用transient修饰的,那么在序列化的时候,他是怎么将包含的dada序列化的呢?

    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
      // Write out any hidden serialization magic
      s.defaultWriteObject();
      
      // Write out size
      s.writeInt(size);
      
      // Write out all elements in the proper order.
      for (Node<E> x = first; x != null; x = x.next)
        s.writeObject(x.item);
    }
    
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
      // Read in any hidden serialization magic
      s.defaultReadObject();
      
      // Read in size
      int size = s.readInt();
      
      // Read in all elements in the proper order.
      for (int i = 0; i < size; i++)
        linkLast((E)s.readObject());
    }
    

    可以看到序列化的时候是将sizenode中的data提取出来,放入java.io.ObjectInputStream,这样就避免了很多结构性的数据传输。

    四、遍历

    关于LinkedList的遍历这里有一个经常都会踩的坑需要注意一下。

    1. 随机访问
    for (int i=0, len = list.size(); i < len; i++) {
     String s = list.get(i);
    }
    
    1. 迭代器遍历
    Iterator iter = list.iterator();
    while (iter.hasNext()) {
     String s = (String)iter.next();
    }
    
    1. 增强for循环遍历
    for (String s : list) {
     ...
    }
    
    1. 效率测试

    对一个LinkedList做顺序遍历:

    1000 10000 100000 200000
    随机访问 2 101 13805 105930
    增强for循环 1 1 3 3
    迭代器 1 1 3 3

    这里可以明显的看增强for循环和迭代器的效率差不多,这是因为增强for循环也是通过迭代器实现的,具体可以查看我在ArrayList章节的分析;但是随机访问的方式遍历,耗时在急剧增加,这是为什么呢?

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

    这里可以看到get(int index)是使用node(int index)实现的,所以对于迭代器和增强for循环遍历时间复杂度是O(n),而使用随机访问的方式遍历时间复杂度则是O(n*n);所以在对LinkedList遍历的时候一定不能采用随机访问的方式。建议直接使用增强for循环遍历,如果一定要使用随机访问则需要判断是否实现RandomAccess接口。

    五、插入和删除

    网上一般都在说LinkedListArrayList的插入和删除快,因为ArryList基于数组,需要移动后续元素,而LinkedList则只需要修改两条引用;但是实际如何呢?

    private static final Random RANDOM = new Random();
    private static List<String> getList(List<String> list, int n) {
      for (int i = 0; i < n; i++) {
        list.add("a" + i);
      }
      return list;
    }
    
    private static long test(List<String> list, int count) {
      long start = System.currentTimeMillis();
      for (int i = 0; i < count; i++) {
        list.add(RANDOM.nextInt(list.size()), i + "");
        list.remove(RANDOM.nextInt(list.size()));
      }
      return System.currentTimeMillis() - start;
    }
    
    public static void main(String[] args) {
      int[] tt = {1000, 10000, 50000};
      
      for (int t : tt) {
        List<String> linked = getList(new LinkedList<>(), t);
        List<String> array = getList(new ArrayList<>(), t);
        System.out.println("------------" + t);
        System.out.println("--linked: " + test(linked, t));
        System.out.println("--array:" + test(array, t));
      }
    }
    

    这里只是简单测试一下,如果需要精确结果可以使用JMH基准测试,
    这里是对长度为 n 的 List,进行随机插入和删除 n 次,结果如下:

    1000 10000 50000
    LinkedList 6 502 18379
    ArrayList 2 9 202

    如果只是在 List 的首尾插入和删除呢,测试结果如下:

    1000 10000 50000
    LinkedList 1 4 9
    ArrayList 2 11 200

    根据测试结果:

    • 对于随机插入和删除,LinkedList效率低于ArrayList;主要是因为LinkedList遍历定位的时候比较慢,而ArrayList是基于数组,可以通过偏移量直接定位,并且ArrayList在插入和删除时,移动数组是通过System.arraycopy完成的,jvm 有做特殊优化,效率比较高。

    • 对于首尾的插入和删除,LinkedList效率高于ArrayList,这里因为LinkedList只需要插入删除一个节点就可以,但ArrayList需要移动数组,同时可能还需要扩容操作,所以比较慢。

    总结

    • LinkedList 基于双向循环链表实现,随机访问比较慢,所以在遍历 List 的时候一定要注意。
    • LinkedList 可以添加重复元素,可以添加 null。
  • 相关阅读:
    快速开始
    阿里为什么选择RocketMQ
    4 分布式消息队列的协调者
    9 首个Apache中间件顶级项目
    3、用适合的方式发送和接收消息
    2 生产环境下的配置和使用
    TOMCAT加载两次war包(重复加载)
    Google Protocol Buffer 的使用(二)
    Google Protocol Buffer 的使用(一)
    PostgreSQL及PostGIS使用
  • 原文地址:https://www.cnblogs.com/sanzao/p/10180364.html
Copyright © 2020-2023  润新知