java集合——LinkedList(1.8)源码浅析

LinkedList是Java集合框架中List接口一种实现。LinkedList 是有序并且可以元素重复的集合,底层是基于双向链表的,它也可以被当作堆栈、队列或双端队列进行操作。

一、继承体系

这里使用IntelliJ IDEA生成的体系图,看一下LinkedList 的继承体系:

在这里插入图片描述
由体系图可以看到,LinkedLis实现了以下接口:

  • 实现了Iterable 接口,可以使用迭代器进行遍历;
  • 实现了Collection接口,拥有集合操作的方法;
  • 实现了List 接口,拥有集合增删改查等方法;
  • 实现Deque/Queue接口,可以当作队列/双端队列使用;
  • 实现了Serializable序列化接口,支持序列化;
  • 实现了Cloneable 接口,能被克隆。

二、主要特点

  1. LinkedList底层数据结构是双向链表;
  2. LinkedList集合中的元素允许为 null;
  3. LinkedList允许内部元素重复;
  4. 元素在LinkedList内部是有序存放的;
  5. LinkedList 与ArrayList相比不存在扩容问题;
  6. LinkedList 实现了栈和队列的操作方法,因此也可以作为栈、队列和双端队列来使用;
  7. LinkedList 由于使用链表实现,在查找的时候,会先判断是在前半部分或者后半部分,然后依次从前或者从后查找,因此查找效率比较慢,增删效率比较快;
  8. LinkedList 线程不安全,可以用个 Collections.SynchronizedList(new LinkedList()) 返回一个线程安全的集合。

三、源码浅析

上面已经总结了LinkedList的主要特点,下面简单解析一下相关源码,增加对其了解。

  1. LinkedList成员变量:
	// LinkedList集合大小    
	transient int size = 0;
	// 头结点
    transient Node<E> first;
	// 尾结点
    transient Node<E> last;

LinkedList成员变量比较简单,只有三个,分别记录集合大小,头结点和尾结点,上面的Node是节点对象,是LinkedList的内部类,源码如下:

    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;
        }
    }

Node对象的结构本身也比较简单,如果你了解过双链表结构应该会很熟悉,每个节点对象除了保存数据元素本身之前,还会保存前一个节点对象和后一个节点对象。

  1. LinkedList构造函数:
	// 无参构造函数
  	public LinkedList() {
  	}
	
	// 有参构造函数
	public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

LinkedList提供了两个构造函数,无参构造函数没有什么好说的,重点看一下public LinkedList(Collection<? extends E> c)构造函数,使用此构造函数可以构造一个包含指定Collection中所有元素的列表,该构造方法首先会调用空的构造方法,然后通过 addAll() 的方式把 Collection 中的所有元素添加进去。

看一下addAll()方法:

 public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

可以看到底层是依赖了addAll(size, c)方法,继续跟踪一下:

public boolean addAll(int index, Collection<? extends E> c) {
		// 检查index是否越界
        checkPositionIndex(index);
		// 将需要放入的集合转成数组
        Object[] a = c.toArray();
        int numNew = a.length;
		//如果是空集合(数组),则直接返回
        if (numNew == 0)
            return false;
		// 声明两个Node对象,pred当前节点表示上一个节点,succ表示当前节点
        Node<E> pred, succ;
		// 如果index和元素个数size相等,说明可以直接从尾结点插入
        if (index == size) {
            succ = null;
            pred = last; // 当前节点的上一个节点就是之前的尾结点
        } else {
            succ = node(index); // 找到index索引位置的节点
            pred = succ.prev; 
        }
		// 遍历数组将对应的元素包装成节点添加到链表中
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
			//构造一个节点对象
            Node<E> newNode = new Node<>(pred, e, null);
			// 如果 pred 为空表示 LinkedList 集合中还没有元素
       		//生成的第一个节点将作为头节点 赋值给 first 成员变量
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            // 节点下移一个
            pred = newNode;
        }
		// 如果 index 位置的元素为 null 则遍历数组后 pred 所指向的节点即为新链表的末节点,赋值给 last 成员变量
        if (succ == null) {
            last = pred;
        } else {
		// 否则将 pred 的 next 索引指向 succ ,succ 的 prev 索引指向 pred(相当于把之后的节点元素串联起来)
            pred.next = succ;
            succ.prev = pred;
        }
		// 更新当前链表的长度 size 并返回 true 表示添加成功
        size += numNew;
		// 记录一下修改次数
        modCount++;
        return true;
    }

上面的方法已经使用比较详细的注释进行了解释,还是比较好理解的。此方法中包含的额外方法,一个是进行index越界判断,源码如下:

 	private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
	private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

另一个是获取index索引节点位置的方法:

Node<E> node(int index) {
        // assert isElementIndex(index);
       // 判断index是否小于size的一半,如果是,则从头结点开始查找
        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;
        }
    }

获取到index所在的节点对象后,可以方便获取前一个(index-1)节点的位置,然后循环将数组中的元素封装为节点添加到链表中就可以了。

  1. LinkedList常见的添加元素方法:
 // 头部添加
 public void addFirst(E e) {
        linkFirst(e);
    }
   //尾部添加
 public void addLast(E e) {
        linkLast(e);
    }
 //直接添加
    public boolean add(E e) {
      linkLast(e);
      return true;
  }
  //指定位置添加
   public void add(int index, E element) {
   	// 检查index越界情况
      checkPositionIndex(index);

      if (index == size)
          linkLast(element);
      else
          linkBefore(element, node(index));
  }

可以看到添加方法,主要依赖link开头的方法实现,这里分别介绍一下:

  • linkFirst方法:
private void linkFirst(E e) {
   // 添加元素之前的头节点
   final Node<E> f = first;
   //以添加的元素为节点值构建新的头节点 并将 next 指针指向 之前的头节点
   final Node<E> newNode = new Node<>(null, e, f);
   // first 索引指向将新的节点
   first = newNode;
   // 如果添加之前链表空则新的节点也作为未节点
   if (f == null)
       last = newNode;
   else
       f.prev = newNode;//否则之前头节点的 prev 指针指向新节点
   size++;
   modCount++;//操作数++
}
  • linkLast方法:
void linkLast(E e) {
   final Node<E> l = last;//保存之前的未节点
   //构建新的未节点,并将新节点 prev 指针指向 之前的未节点
   final Node<E> newNode = new Node<>(l, e, null);
   //last 索引指向末节点
   last = newNode;
   if (l == null)//如果之前链表为空则新节点也作为头节点
       first = newNode;
   else//否则将之前的未节点的 next 指针指向新节点
       l.next = newNode;
   size++;
   modCount++;//操作数++
}
  • linkBefore方法:
void linkBefore(E e, Node<E> succ) {
   // assert succ != null;
   // 由于 succ 一定不为空,所以可以直接获取 prev 节点
   final Node<E> pred = succ.prev;
   // 新节点 prev 节点为 pred,next 节点为 succ
   final Node<E> newNode = new Node<>(pred, e, succ);
   // 原节点的 prev 指向新节点
   succ.prev = newNode;
   // 如果 pred 为空即头节点出插入了一个节点,则将新的节点赋值给 first 索引
   if (pred == null)
       first = newNode;
   else
       pred.next = newNode;//否则 pred 的下一个节点改为新节点
   size++;
   modCount++;
}
  1. LinkedList常见的删除元素方法:
// 删除头节点
 public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
 }

// 删除尾节点
 public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
 }
 
// 删除指定元素
public boolean remove(Object o) {
        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;
    }
// 删除指定索引元素
 public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

可以看到删除节点方法,底层主要依赖unlink开头的几个方法,这里也简单介绍一下:

  • unlinkFirst方法
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    // 头节点的 element 这里作为返回值使用
    final E element = f.item;
    // 头节点下个节点
    final Node<E> next = f.next;
    // 释放头节点的 next 指针,和 element 下次 gc 的时候回收这个内部类
    f.item = null;
    f.next = null; // help GC
    // 将 first 索引指向新的节点
    first = next;
    // 如果 next 节点为空,即链表只有一个节点的时候,last 指向 null
    if (next == null)
        last = null;
    else
        next.prev = null; //否则 next 的 prev 指针指向 null
    size--;//改变链表长度
    modCount++;//修改操作数
    return element;//返回删除节点的值
 }
  • unlinkLast方法:
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    //未节点的前一个节点,
    final Node<E> prev = l.prev;
    //释放未节点的内容
    l.item = null;
    l.prev = null; // help GC
    //将 last 索引指向新的未节点
    last = prev;
    // 链表只有一个节点的时候,first 指向 null
    if (prev == null)
       first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
 }
  • unlink方法:
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;
    }

这里额外简单介绍一下clear方法:

public void clear() {
   // 依次清除节点,帮助释放内存空间
   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++;
}
  1. LinkedList常见的元素查询方法:
// 根据索引查询
public E get(int index) {
   checkElementIndex(index);
   return node(index).item;
}

// 返回 first 索引指向的节点的内容,如果链表为空则抛出异常
public E getFirst() {
   final Node<E> f = first;
   if (f == null)
       throw new NoSuchElementException();
   return f.item;
}

// 返回 first 索引指向的节点的内容,如果链表为空不会抛出异常
public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
}

// 返回 last 索引指向的节点的内容,如果链表为空则抛出异常
public E getLast() {
   final Node<E> l = last;
   if (l == null)
       throw new NoSuchElementException();
   return l.item;
}

// 返回 last 索引指向的节点的内容,如果链表为空不会抛出异常
public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }

上述方法比较简单,这里不再赘述了。

  1. LinkedList常见的节点修改方法:
  • set方法:
public E set(int index, E element) {
   // 判断角标是否越界
   checkElementIndex(index);
   // 采用 node 方法查找对应索引的节点
   Node<E> x = node(index);
   //保存节点原有的内容值
   E oldVal = x.item;
   // 设置新值
   x.item = element;
   // 返回旧的值
   return oldVal;
}
  1. LinkedList常见元素索引查询方法:
// 返回参数元素在链表的节点索引,如果有重复元素,那么返回值为从**头节点**起的第一相同的元素节点索引, 如果没有值为该元素的节点,则返回 -1;
public int indexOf(Object o) {
   int index = 0;
  // 区别对待 null 元素,用 == 判断,非空元素用 equels 方法判断 
   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;
}

// 返回参数元素在链表的节点索引,如果有重复元素,那么返回值为从**尾节点起**的第一相同的元素节点索引,如果没有值为该元素的节点,则返回 -1;
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实现了Deque和Queue接口,所以获取、删除队列头部元素的方法不唯一,具体可自行查看LinkedList的方法实现。

四、LinkedList遍历

LinkedList 没有单独 Iterator 实现类,它的 iterator 和 listIterator 方法均返回 ListItr的一个对象。

private class ListItr implements ListIterator<E> {
   // 上一个遍历的节点
   private Node<E> lastReturned;
   // 下一次遍历返回的节点
   private Node<E> next;
   // cursor 指针下一次遍历返回的节点
   private int nextIndex;
   // 期望的操作数
   private int expectedModCount = modCount;
    
   // 根据参数 index 确定生成的迭代器 cursor 的位置
   ListItr(int index) {
       // assert isPositionIndex(index);
       // 如果 index == size 则 next 为 null 否则寻找 index 位置的节点
       next = (index == size) ? null : node(index);
       nextIndex = index;
   }

   // 判断指针是否还可以移动
   public boolean hasNext() {
       return nextIndex < size;
   }
    
  // 返回下一个带遍历的元素
  public E next() {
       // 检查操作数是否合法
       checkForComodification();
       // 如果 hasNext 返回 false 抛出异常,所以我们在调用 next 前应先调用 hasNext 检查
       if (!hasNext())
           throw new NoSuchElementException();
        // 移动 lastReturned 指针
       lastReturned = next;
        // 移动 next 指针
       next = next.next;
       // 移动 nextIndex cursor
       nextIndex++;
       // 返回移动后 lastReturned
       return lastReturned.item;
   }

  // 当前游标位置是否还有前一个元素
   public boolean hasPrevious() {
       return nextIndex > 0;
   }
  
  // 当前游标位置的前一个元素
   public E previous() {
       checkForComodification();
       if (!hasPrevious())
           throw new NoSuchElementException();
        // 等同于 lastReturned = next;next = (next == null) ? last : next.prev;
        // 发生在 index = size 时
       lastReturned = next = (next == null) ? last : next.prev;
       nextIndex--;
       return lastReturned.item;
   }
    
   public int nextIndex() {
       return nextIndex;
   }

   public int previousIndex() {
       return nextIndex - 1;
   }
    
    // 删除链表当前节点也就是调用 next/previous 返回的这节点,也就 lastReturned
   public void remove() {
       checkForComodification();
       if (lastReturned == null)
           throw new IllegalStateException();

       Node<E> lastNext = lastReturned.next;
       //调用LinkedList 的删除节点的方法
       unlink(lastReturned);
       if (next == lastReturned)
           next = lastNext;
       else
           nextIndex--;
       //上一次所操作的 节点置位空    
       lastReturned = null;
       expectedModCount++;
   }

    // 设置当前遍历的节点的值
   public void set(E e) {
       if (lastReturned == null)
           throw new IllegalStateException();
       checkForComodification();
       lastReturned.item = e;
   }
    // 在 next 节点位置插入及节点
   public void add(E e) {
       checkForComodification();
       lastReturned = null;
       if (next == null)
           linkLast(e);
       else
           linkBefore(e, next);
       nextIndex++;
       expectedModCount++;
   }
    //简单哈操作数是否合法
   final void checkForComodification() {
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
   }
}

LinkedList常见的遍历方式如下:

首先,初始化一个LinkedList:

LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 10000; i++) {
    list.add(i);
}
  • 使用stream的方式:
list.stream().forEach((t) -> System.out.println(t));
  • 使用迭代器,即通过Iterator去遍历:
for (Iterator iter = list.iterator(); iter.hasNext(); ) {
            System.out.println(iter.next());
}
  • 通过快速随机访问遍历:
for (int i = 0, len = list.size(); i < len; i++) {
            System.out.println(list.get(i));
}
  • 通过for-Each来遍历:
for (Integer i : list) {
            System.out.println(i);
 }
  • 通过pollFirst()来遍历:
for(Integer i = list.pollFirst() ;i != null;i = list.pollFirst()){
            System.out.println(i);
}
  • 通过pollLast()来遍历:
for(Integer i = list.pollLast() ;i != null;i = list.pollLast()){
            System.out.println(i);
}
  • 通过removeFirst()来遍历:
try {
         for (Integer i = list.removeFirst(); i != null; i = list.removeFirst() ; ){
             System.out.print(i);
         }
} catch (Exception e) {

 }
  • 通过removeLast()来遍历:
try {
       for (Integer i = list.removeLast(); i != null; i = list.removeLast()) {
           System.out.print(i);
       }
 } catch (Exception e) {

 }

通过测试可以发现,遍历LinkedList时,使用removeFist()或removeLast()耗时最短。但用它们遍历时,会删除原始数据;若单纯只读取,而不删除,可以使用For-Each的方式读取,另外,尽量不要通过随机访问(for循环)去遍历LinkedList!

本文只是简单的解析了一下LinkedList的常见方法和源码,如果想要了解更多,可以自行查看相关源码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码探险家_cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值