Java集合List系列(二):LinkedList源码解析(JDK1.8)

图1-1 思维导图

概述

LinkedList是基于节点实现的 双向 链表的 List,每个节点都指向前一个和后一个节点从形成链表。

类图

图 1-2 LinkedList类图

从图中我们可以看出, LinkedList实现了4个接口和继承了1个抽象类,4个接口分别为:

  • List接口,主要提供添加、删除、修改、迭代遍历等操作。

  • Cloneable接口,代表 LinkedList支持克隆。

  • Serializable接口,表示ArrayList支持序列化的功能。

  • Deque接口,提供 双端 队列的功能,LinkedList支持快速的在头尾添加和读取元素。

LinedList所实现的接口相比于 ArrayList来说,少了一个 RandomAccess接口,说明 LinkedList并不支持随机访问;同时多了一个 Deque接口,新增了双端队列的能力。

继承的抽象类为 AbstactSequentialList,它是 AbstractList的子类,实现了本来只能 顺序 访问的数据存储结构(例如:链表)的 get(int index)、 add(int index, E element)随机 操作的方法。这句话可能有点绕,稍加解释一番:链表是一种只能顺序访问的数据存储结构,而 AbstractSequentialList抽象类对这类只能 顺序访问/操作 的数据存储结构,也提供了类数组般的随机访问/操作API。感兴趣的朋友可以去看看 AbstractSequentialList的源代码,其是基于迭代器顺序遍历(说到底还是需要遍历,只不过是它帮我们做了这一步~)实现的。

不过需要留意的是,LinkedListLinkedList一般,都结合了自身的特性大量重写了抽象类提供的方法实现;ArrayList重写了 AbstractList提供的方法实现, LinkedList重写了 AbstactSequentialList提供的方法实现。

一般情况下,对于支持随机访问的数据结构会继承 AbstractList抽象类,不支持随机访问的则会继承 AbstactSequentialList抽象类。

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

属性

LinkedList只有 3 个属性,分别为: 头节点 first、 尾节点 last以及代表数量的 size。三者的关系如下图所示。

图1-3 属性关系图

相信朋友们都接触过双向链表的概念,这里就不做过多的展开解释了。

具体源码如下:

/**
  * 头节点
  */
transient Node<E> first;

/**
  * 尾节点
*/
transient Node<E> last;

/**
  * 数量
  */
transient int size = 0;

/**
  * 底层实现是 双向链表
  */
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的构造方法只有2个:无参构造方法 public ListedList()和指定集合的 public LinkedList(Collection<? extends E> c),具体代码如下:


/**
  * 无参构造方法
  */
public LinkedList() {
}

/**
  * 指定添加集合c的构造方法
  * @param c
  */
public LinkedList(Collection<? extends E> c) {
    this();
    // 批量添加元素,后面在新增元素模块会详细介绍
    addAll(c);
}

相比于 ArrayList来说, LinkedList没有类似指定初始化容量的构造方法;造成这个区别的原因还是因为两者的底层实现方式不一样,LinkedList是基于节点实现的双向链表,所以每次添加元素的时候直接 new一个节点即可。

新增元素

新增元素的方法主要有如下4个,分别为:

  • public boolean add(E e)在链表节点后 顺序 添加一个元素

  • public void add(int index, E element)在指定 index位置添加元素 element

  • public boolean addAll(Collection<? extends E> c)在链表末尾 顺序 添加多个元素

  • public boolean addAll(int index, Collection<? extends E> c)在指定 index位置添加多个元素

严格来说,LinkedList的新增元素的方法还有另外 2 个,分别为: addFirst(E e)addLast(E e);这两个方法是 LinkedList作为实现双端队列 Deque的产物,后面会对 LinkedList作为双端队列的方法做专门的介绍。

public boolean add(E e)

继承于 AbstractList抽象类,在链表节点后 顺序 添加一个元素,源码如下:

/**
  * 在链表节点后顺序添加一个元素e
  * @param e
  * @return boolean 添加成功返回true
  */
@Override
public boolean add(E e) {
  // 顺序添加单个元素到链表的末尾
  linkLast(e);
  return true;
}

/**
  * 真实执行添加操作(往链表最后添加新的元素)
  * @param e 添加元素 e
  */
void linkLast(E e) {
  // 记录原 last 节点
  final Node<E> l = last;
  // Node类构造方法三个参数: Node<E> prev, E element, Node<E> next
  // 所以也很好理解,新节点的前置pre节点是原来的last节点, e代表元素本身 新节点的next节点为null(不存在新的节点)
  final Node<E> newNode = new Node<>(l, e, null);
  // 更新last节点为最新的newNode
  last = newNode;
  // 双向链表,相互关联---新节点关联原last节点,原last节点也要关联新的节点
  if (l == null) {
      // l == null,说明是一个空的情况,上面已经将last指向新节点了,需要将first也指向新节点
      first = newNode;
  } else {
      // 不为空,则说明链表中已有元素,直接将last节点和new节点做一个双向的绑定关系即可
      l.next = newNode;
  }
  size++;
  // 操作的次数++,全程似乎就在序列化和非序列化模块有瞅到这个东东被使用到
  modCount++;
}

代码理解起来不难,往链表末尾新增一个新的节点,可分3步走:

  1. 创建新节点 new Node<>(last, e, null),参数 1 为 last是说新节点的前置节点是原last节点,参数2 代表元素本身e;参数3 为 null说明新节点后面无其他节点

  2. 原last指针更新为新节点,这里分2种情况:

    1. 原last节点为空,说明链表还未存储过元素,这时候需要将first节点也指向新节点

    2. 原last节点不为空,说明链表已存储过元素,直接将原last节点的后置指针 next指向新节点(因为其为双向链表)

  3. 更新 size和 操作次数 modCount

public void add(int index, E element)

继承于 AbstractSequentialList抽象类,为以双向链表为底层存储结构的 LinkedList赋予了类数组般随机访问的能力,具体代码如下:

 /**
  * 添加单个元素到指定位置
  * @param index 下标
  * @param element 元素本身
  */
@Override
public void add(int index, E element) {
    // 参数index 有效性检查
    checkPositionIndex(index);

    // index从0开始,所以当index == size 说明是最后的位置,也即是如上面的在链表最后新增元素一样
    if (index == size) {
        linkLast(element);
    } else {
        // node(index) 获取index位置的node节点
        linkBefore(element, node(index));
    }
}
/**
  * 添加元素e到 succ节点的前面
  *
  * pred ---> succ     =====变成===>        pred --->newNode ---> succ
  *
  * 步骤:
  *  1.获取succ节点的前一个节点 pred
  *  2.创建新的待插入节点newNode,指定其前置节点(pred)和后置节点(succ)
  *  3.更新succ的前置节点为新节点newNode(双向绑定的体现)
  *  4.更新pred的后置节点为新节点newNode(双向绑定的体现)
  *      4.1如果pred为空,说明succ本身为头节点,换句话说相当于是在头节点之前插入新的节点,即更新first = newNode即可
  *      4.2如果pred不为空,则正常的pred.next= newNoe即可
  * @param e
  * @param succ
  */
void linkBefore(E e, Node<E> succ) {
    // 获取succ节点的前一个节点(因为方法的逻辑是:添加元素e到succ节点的前面)
    final Node<E> pred = succ.prev;
    // 创建新节点,并且本身关联前后节点(前节点: pred、 后节点:succ)
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 双向绑定之画个图完事 succ.prev指向新的节点(在其前面)
    succ.prev = newNode;
    // 如果pred为空,说明succ本身就是为头节点,则更新first指向newNode作为新的首节点
    if (pred == null) {
        first = newNode;
    } else {
        // 直接双向绑定
        pred.next = newNode;
    }
    size++;
    modCount++;
}

/**
  * 找到下标为index的节点
  * 骚操作: 通过判断index是否大于size的一半,来判断是从头遍历还是从尾遍历
  * @param index
  * @return Node<E>
  */
Node<E> node(int index) {
    // 不太明白为啥把这个断言注释了(源码注释的,不是我注释的~~), 这相当于是参数的合法性检查,
    // assert isElementIndex(index);

    Node<E> x;
    //  index 小于 size的一半,从头遍历更加的便捷
    if (index < (size >> 1)) {
        x = first;
        for (int i = 0; i < index; i++) {
            x = x.next;
        }
    } else {
        // index 大于size的一半,从尾遍历更加的便捷
        x = last;
        for (int i = size - 1; i > index; i--) {
            x = x.prev;
        }
    }
    return x;
}

代码的逻辑还是比较清晰的,相信小伙伴从代码的注释中都能看懂其逻辑,这里就不过度的展开了;值得一提(比较好玩)的是 node(int index)找到下标为 index的节点的方法实现;通过判断 index是否大于 size的一半,来判断是从头遍历还是从尾遍历,很细节。

public boolean addAll(Collection<? extends E> c)

public boolean addAll(Collection<? extends E> c)方法继承于 AbstractCollection抽象类,往集合后面添加多个元素,源码如下:

/**
    * 顺序添加多个元素
    * @param c
    * @return boolean
    */
  @Override
  public boolean addAll(Collection<? extends E> c) {
      // addAll(int index, c) 在指定index位后添加多个元素
      return addAll(size, c);
  }

这个方法调用的是 public boolean addAll(int index, Collection<? extends E> c)来实现自己的功能,所以我们直接看真正干活的方法吧~~

public boolean addAll(int index, Collection<? extends E> c)

public boolean addAll(int index, Collection<? extends E> c)方法继承于 AbstractSequentialList抽象类,赋予了 LinkedList类似 ArrayList一般 随机 添加元素的能力,具体源码如下:

/**
  * 在指定index位添加集合元素
  * index从0开始算起,并且index位就是新添加的元素了
  * 具体步骤:
  *  1.前置操作----参数检查等
  *  2.找到index位的前置节点prev 和 后置节点succ
  *  3.构建子Link链路
  *  4.连贯原有链路和子Link链路
  *
  * @param index
  * @param c
  * @return boolean
  */
@Override
public boolean addAll(int index, Collection<? extends E> c) {
    // 步骤1:各种参数合法性检查
    checkPositionIndex(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    // 如果无添加元素,直接返回false 代表未变更
    if (numNew == 0) {
        return false;
    }
    // 步骤2:pred 代表index 的前一个节点 succ代表index的后一个节点
    Node<E> pred, succ;
    // index等于size,说明是往链表的最后添加元素,则index的后一个元素succ为空,index的前一个元素为last
    // 注意:这里的index其实是一个空的值,并无节点;所以index的前一个节点prev是last
    // 其实这算一种特判
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        // pred---succ(index位)   =======变成======>  pred------newNode(index位)----succ

        // succ是index的后一个节点---因为index位置即将被新节点所取代,所以原来index位置的Node,变成succ
        succ = node(index);
        // pred是index 的前一个节点,
        pred = succ.prev;
    }
    // 步骤3:构建子Link链路
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        // pred一直在更新,制作一个内部链接好的子链路(newNode的前置节点为pred)
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null) {
            // pred为空,说明插入的位置选择是0,首节点
            first = newNode;
        } else {
            // 双向绑定
            pred.next = newNode;
        }
        // 更新pred指针
        pred = newNode;
    }

    // 步骤4: 连贯原有链路和子Link链路,使其串在一起
    // succ为index后的一个节点,如果succ为空,说明插入的位置就是在最后面,则直接设置last为pred(pred一直在更新)
    if (succ == null) {
        last = pred;
    } else {
        // 双向链表之双向绑定
        pred.next = succ;
        succ.prev = pred;
    }
    // 数量添加
    size += numNew;
    // 操作次数++
    modCount++;
    return true;
}

代码可能会有点长,但是实际上逻辑十分清晰;可分为4个步骤:

  1. 前置操作–参数合法性检查

  2. 找到 index位的前置节点 prev和后置节点 succ

  3. 构建子Link链路

  4. 连贯原有链路和子Link链路

至此,LinkedList的添加元素的方法就介绍完毕了,本质上是对链表的新增Node节点操作。小伙伴在阅读源码的时候可以边阅读边画出相关的链表,加深理解。

删除元素

LinkedList的删除元素方法主要有2个,分别为:

  • public boolean remove(Object o)删除首个指定元素o

  • public E remove(int index)删除指定 index位的元素

乍一看,LinkedList似乎没有批量删除元素的方法,找遍 LinkedList.java类的所有API方法确实没有找到相关批量删除的方法,但实际上 LinkedList是有批量删除的功能的。 LinkedList继承的 AbstractSequentialList的父类 AbstractCollection中有提供相关的API,通过迭代器的方式实现。

public boolean remove(Object o)

public boolean remove(Object o)方法继承于 AbstractCollection抽象类,删除链表中 首个 指定元素o,具体源码如下:

/**
  * 删除首个指定元素o
  * @param o
  * @return boolean
  */
@Override
public boolean remove(Object o) {
    // 说明LinkedList支持存放 null
    if (o == null) {
        // 遍历 找到需要的数据 然后删除
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

/**
  * 真实执行删除操作,并返回被删除元素值
  * 做法:将该节点从链路上剔除,并且之节点置null(方便GC)
  * @param x
  * @return E
  */
E unlink(Node<E> x) {
    // 不太知道为啥子注释掉了 参数不做非空判断吗?
//         assert x != null;

    // element 节点的元素
    final E element = x.item;
    // 后一个节点
    final Node<E> next = x.next;
    // 前一个节点
    final Node<E> prev = x.prev;

    // 如果前置节点为空,说明删除的节点是首节点,first指向next
    if (prev == null) {
        first = next;
    } else {
        // prev ---- x --- next  ====变成====>  prev ----- next

        prev.next = next;
        // x.prev 设置为null,断开与原有链路的关联,同时也是为了GC
        x.prev = null;
    }

    // 如果后置节点为空,说明删除的节点是尾节点, last直接指向prev
    if (next == null) {
        last = prev;
    } else {
        // 本来next.prev指向了 x,但是x要被删除,所以next.prev需要指向 x的前一个节点 也即是prev
        next.prev = prev;
        // x.next设置为null,断开与原有链路的关联,同时为了GC
        x.next = null;
    }

    // for GC
    x.item = null;
    size--;
    modCount++;
    // 返回element
    return element;
}

从代码中我们可以看出,真实做删除操作的方法是 unlink(Node<E> x),其代码不难理解,小伙伴们可以看看注释来理解。

public E remove(int index)

public E remove(int index)方法继承 AbstractSequentialList,用以赋能像数组般的 随机 操作的能力,具体源码如下:

/**
  * 删除指定index位置的元素
  * @param index
  * @return E
  */
@Override
public E remove(int index) {
    // index参数合法性检查
    checkElementIndex(index);
    return unlink(node(index));
}

可以看出,其实现方式是先找到 index位置节点的元素,然后调用 unlink(Node e)方法;这两个方法在上面都有过比较详细的介绍,这里就不做过多的赘述。

查找元素

LinkedList中查找元素的方法有2个,分别为:

  • public int indexOf(Object o)方法,查找首个为指定元素 o 的位置

  • public int lastIndexOf(Obejct o )方法,查找最后一个为指定元素 o 的位置

两个方法的源码都很简单,两者的差别在于一个是从头遍历,一个是从尾遍历(因为LinkedList底层实现是双向链表,所以很容易实现这点);具体源代码如下:

/**
  * 查找首个为指定元素 o 的位置
  * @param o
  * @return int 如果不存在则返回-1
  */
@Override
public int indexOf(Object o) {
    int index = 0;
    // 可以看出:LinkedList也是支持存储null值的
    if (o == null) {
        // 遍历链表
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}


/**
  * 查找最后一个为指定元素 o 下标
  * 逆序遍历链表实现
  * @param o
  * @return int 存在则返回对应index 不存在则返回-1
  */
@Override
public int lastIndexOf(Object o) {
    int index = size;
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (x.item == null)
                return index;
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            index--;
            if (o.equals(x.item))
                return index;
        }
    }
    return -1;
}

从源码中我们可以看出,LinkedList是支持存储 null值。

转换成数组

LinkedList的转换成数组的方法和 ArrayList一样,也可以转换成数组,具体的方法如下:

  • public Object[] toArray()LinkedList转换为 Object[]数组

  • public <T> T[] toArray(T[] a)LinkedList转换为指定 T泛型的数组

源码如下:

/**
  * 转换成数组
  * @return Object[]
  */
public Object[] toArray() {
    // 遍历链表,获取值 注意:返回值类型是Object[]
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}


/**
  * 转化成数组(可指定返回泛型)
  * @param a
  * @return T[]
  */
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
    // 如果传入的数组小于size的长度,直接复制一个新的数组返回
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
                a.getClass().getComponentType(), size);
    // 顺序遍历链表,复制到a中
    int i = 0;
    Object[] result = a;
    for (Node<E> x = first; x != null; x = x.next) {
        result[i++] = x.item;
    }

    if (a.length > size) {
        a[size] = null;
    }

    return a;
}

需要关注的点和 ArrayList一样,如果希望返回指定类型的数组,则应该使用 #toArray(T[] a)方法。

其他常见方法

其他比较常见的方法,比如判断集合中是否包含某个元素 contains(Object o)方法、获取指定位置的元素 get(int index)、设置指定位置的元素 set(int index, E element)、清空数组 clear()、还有一个比较有趣但是不咋常用的方法——创建子数组 subList(int fromIndex, int toIndex)

由于比较简单,一次性上(叶问附体:我要一个打十个!!!)

/**
  * 判断元素是否存在
  * 依赖indexOf方法
  */
@Override
public boolean contains(Object o) {
    return indexOf(o) != -1;
}


/**
  * 获取指定index位置的元素
  * @param index
  * @return E
  */
@Override
public E get(int index) {
    // index参数合法性检查
    checkElementIndex(index);
    // 直接返回~
    return node(index).item;
}

/**
  * 设置指定位置index位置元素element,并返回旧值
  * @param index
  * @param element
  * @return E
  */
public E set(int index, E element) {
    // index参数合法性检查
    checkElementIndex(index);
    // 获取node节点
    Node<E> x = node(index);
    // 替换旧值
    E oldVal = x.item;
    x.item = element;
    // 返回旧值
    return oldVal;
}


/**
  * 清空集合
  */
@Override
public void clear() {
    // 顺序遍历链表,设置每个节点前后都指向null 通过这种方式 帮助GC
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}

// AbstractList.java

/**
  * [fromIndex, toIndex)
  * 根据判断 RandomAccess 接口,判断是否支持随机访问,从而创建 RandomAccessSubList 或 SubList 对象
  * 创建子List(注意:和父List共享同一份数据,也即是子List的修改会影响到父List)
  * @param fromIndex 可达
  * @param toIndex 不可达
  */
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size());
    // 根据判断 RandomAccess 接口,判断是否支持随机访问
    return (this instanceof RandomAccess ?
            new RandomAccessSubList<>(this, fromIndex, toIndex) :
            new SubList<>(this, fromIndex, toIndex));
}

LinkedList实现栈、队列、双端队列分析

LinkedList的类图我们可以知道,LinkedList实现了 Deque双端队列接口,我们来看看Deque的类图,掰扯掰扯。

图1-4 Deque类图

从类图来看,Deque接口继承于 Queue接口,所以 LinkedList拥有队列、双端队列的特性;其次JAVA中有一个过时类 Stack代表栈,JAVA中并没有单独的栈接口,但是栈相关的方法包括在了 Deque接口中,因此 LinkedList同时也拥有栈的特性。

图 1-5 LinkedList的特殊功能

双端队列方法

/**
  * =======================================双端队列==============================
  */

/**
  * 往双端队列 前端添加元素
  */
@Override
public void addFirst(E e) {
    linkFirst(e);
}

/**
  * 双端队列 末尾添加元素
  */
@Override
public void addLast(E e) {
    linkLast(e);
}

/**
  * 双端队列前端添加元素
  */
@Override
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

/**
  * 双端队列 末尾端添加元素
  */
@Override
public boolean offerLast(E e) {
    addLast(e);
    return true;
}

/**
  * 双端队列 前端移除首个元素
  */
@Override
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

/**
  * 双端队列 末尾端 移除首个元素
  */
@Override
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

/**
  * 双端队列  前端 弹出首个元素
  */
@Override
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

/**
  * 双端队列  末尾端 弹出首个元素
  */
@Override
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}

/**
  * 双端队列 获取首端 首个元素
  */
@Override
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

/**
  * 双端队列 获取末尾端 首个元素
  */
@Override
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

@Override
public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

@Override
public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}


@Override
public boolean removeFirstOccurrence(Object o) {
    return remove(o);
}

@Override
public boolean removeLastOccurrence(Object o) {
    if (o == null) {
        for (Node<E> x = last; x != null; x = x.prev) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

代码都比较的简单,不需要过多的说明,小伙伴看看就行。

队列方法

/**
  * ==================================队列方法========================================
  */

@Override
public boolean add(E e) {
    // 顺序添加单个元素到链表的末尾
    linkLast(e);
    return true;
}

@Override
public boolean offer(E e) {
    return add(e);
}

@Override
public E remove() {
    // 移除第一个元素,Deque接口的方法
    return removeFirst();
}

@Override
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

@Override
public E element() {
    return getFirst();
}

@Override
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

栈方法

/**
  * ==================================栈方法========================================
  */
@Override
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}


@Override
public void push(E e) {
    addFirst(e);
}

@Override
public E pop() {
    return removeFirst();
}

遍历方式探讨

LinkedList支持如下几种遍历方式,下面逐个讨论:

  • 通过 Iterator迭代器遍历
Integer value = null;
Iterator it = list.iterator();
while (it.hasNext()) {
    value = (Integer)it.next();
}
  • 通过 快速随机访问 遍历 (注意:并不建议采用这种方式遍历)
Integer value = null;
int size = list.size();
for (int i = 0; i < size; i++) {
    value = (Integer)list.get(i);
}
  • 通过 for循环 遍历

Integer value = null;
for (Integer integ : list) {
    value = integ;
}

其实经过我们上面的分析,我们都应该明白:不应该使用随机访问的方式去遍历 LinkedList,因为其底层实现上采用的是 双向链表,即使提供了类似数组般随机访问的API,实际上还是通过挨个节点挨个节点遍历来达到随机访问的效果的(具体可看看上面的get(int index)方法源码解析);官方更推荐使用顺序访问,也即是通过迭代器或者 for循环 的方式来遍历。

参考

加入了芋道源码 大佬的知识星球,文章的分析有借鉴大佬的思路。

总结

本文主要从源码层面讲解了 LinkedList的实现方式,LinkedList底层是基于节点实现的双向链表;是一个直线型的链表结构。

LinkedList只有 3 个属性,分别是代表头节点的 first、代表尾节点的 last以及表示数量的 size

LinkedList有 2 个构造方法,分别是无参的构造方法 LinkedList()和指定元素集的构造方法 LinkedList(Collection<? extends E> c),因为其底层实现上采用的是双向链表,因此它并没有像 ArrayList指定初始化容量的构造方法,也没有需要扩容的操作;每次新增元素直接 new一个新的节点插入到双向链表中即可。

  • LinkedList的新增元素和删除元素操作都是一些链表操作,理解起来相对比较的简单;另外由于 LinkedList继承了 AbstactSequentialList抽象类,因此被赋予了类似数组般的随机访问/操作的能力(实际上还是通过逐个遍历的方式实现的)。

  • 因为 LinkedList是基于节点实现的,故而有一定的资源消耗(前后指针),但是如果存储的元素(对象)element足够大的话,那么这点开销可忽略不记的。

  • 最后需要留意的是,我们在分析源码的时候,是发现 LinkedList是支持存储 null的。

文章到这就结束拉,欢迎大家扫码关注小奇公众号~
请添加图片描述

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值