Java源码学习笔记(2):LinkedList

LinkedList

  底层数据结构是双向链表,如图。链表中没数据时,first和last是同一个结点,前后指向null。因为是个双向链表,只要机器内存足够大,没有大小限制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OzywFitY-1581256004798)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191211153542310.png)]

内部有一个Node类

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

1、 新增

  追加节点时,我们可以选择追加到链表头部,还是追加到链表尾部,add 方法默认是从尾部开始追加,通过移动尾节点的 next 指向,addFirst 方法是从头部开始追加,通过移动头节点的 prev 指向。

    //从尾部增加
		public boolean add(E e) {
        linkLast(e);
        return true;
    }
    void linkLast(E e) {
      	//把尾节点数据暂存
        final Node<E> l = last;
      	//新建节点,l是新节点的前驱,e为要新增的节点,新增节点的后一个节点为null
        final Node<E> newNode = new Node<>(l, e, null); //(前驱、本身、后继)
      	//把newNode追加到尾部
        last = newNode;
      	//链表空与不空两种情况
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
      	//大小与版本的修改
        size++;
        modCount++;
    }

  	//从头部增加
    public void addFirst(E e) {
        linkFirst(e);
    }
    private void linkFirst(E e) {
      	//头节点赋给临时变量
        final Node<E> f = first;
      	//新建节点,新节点前驱为null,e是新建节点,f为后继
        final Node<E> newNode = new Node<>(null, e, f); //(前驱、本身、后继)
        first = newNode;
      	//判断链表空不空
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
      	//修改大小与版本
        size++;
        modCount++;
    }

2、 删除

  节点删除的方式和追加类似,我们可以选择从头部删除,也可以选择从尾部删除,删除操作会把节点的值,前后指向节点都置为 null,帮助 GC 进行回收。贴一个从头部删除

    public E remove() {
        return removeFirst();
    }
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    private E unlinkFirst(Node<E> f) {
        //拿出头节点的值作为方法的返回值
        final E element = f.item;
      	//拿出头节点的下一个节点
        final Node<E> next = f.next;
      	//帮助GC回收
        f.item = null;
        f.next = null; // help GC
      	//头节点的下一个节点成为头节点
        first = next;
      	//如果 next 为空,表明链表为空
        if (next == null)
            last = null;
        else
          	//链表不为空,头节点的前一个节点指向 null
            next.prev = null;
      	//修改大小与版本
        size--;
        modCount++;
        return element;
    }

3、 查询

  链表查询某一个节点是比较慢的,需要挨个循环查找才行。

    Node<E> node(int index) {
				//如果 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;
        }

  LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分 法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻 找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能。

4、 方法对比

方法含义 返回异常 返回特殊值 底层实现
新增节点 add(e) offer(e) 底层实现相同
删除节点 remove(e) poll(e) 链表为空时,remove 会抛出异常,poll 返回 null。
查找节点 element(e) peek() 链表为空时,element 会抛出异常,peek 返回 null。
public class LinkedList<E> extends AbstractSequentialList<E>
   implements List<E>, Deque<E>, Cloneable, java.io.Serializable

  LinkedList 也实现了 Deque 接口,对新增、删除和查找都提供从头开始,还是从尾开始两种方向的方法,比如 remove 方法,Deque 提供了 removeFirst 和 removeLast 两种方向的使用方式,但当链表为空时的表现都和 remove 方法一样,都会抛出异常。

5、 迭代器

  因为 LinkedList 要实现双向的迭代访问,所以我们使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 新增了一个迭代接口,叫做ListIterator,这个接口提供了向前和向后的迭代方法。

迭代顺序 方法
从头到尾迭代方法 hasNext、next、nextIndex
从尾到头迭代方法 hasPrevious、previous、previousIndex
    private class ListItr implements ListIterator<E> {
        private Node<E> lastReturned;	//上一次执行 next() 或者 previos() 方法时的节点位置
        private Node<E> next;	//下一个节点
        private int nextIndex;	//下一个节点的位置
        private int expectedModCount = modCount;	//期望版本号与目标版本号
      
        public boolean hasNext() {
            return nextIndex < size;	// 下一个节点的索引小于链表的大小,就有
        }

        public E next() {
          	//检查期望版本号有无发生变化
            checkForComodificaction();
          	//再次检查
            if (!hasNext())
                throw new NoSuchElementException();
						//把上一个节点位置改为当前节点
            lastReturned = next;
          	// next 是下一个节点了,为下次迭代做准备
            next = next.next;
            nextIndex++;
          	//返回节点值
            return lastReturned.item;
        }

       //从尾到头迭代	
        public boolean hasPrevious() {
            return nextIndex > 0;	// 如果上次节点索引位置大于 0,就还有节点可以迭代
        }
				// 取前一个节点
        public E previous() {
            checkForComodification();
            if (!hasPrevious())
                throw new NoSuchElementException();
						// next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
          	// next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
            lastReturned = next = (next == null) ? last : next.prev;
          	// 索引位置变化
            nextIndex--;
            return lastReturned.item;
        }
      
      	//迭代器删除
        public void remove() {
            checkForComodification();
          	// lastReturned 是本次迭代需要删除的值,分以下空和非空两种情况:
						// lastReturned 为空,说明调用者没有主动执行过 next() 或者 previos(),直接报错
          	// lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
            if (lastReturned == null)
                throw new IllegalStateException();

            Node<E> lastNext = lastReturned.next;
          	//删除当前节点
            unlink(lastReturned);
            if (next == lastReturned)
                next = lastNext;
            else
                nextIndex--;
            lastReturned = null;
            expectedModCount++;
        }

6、 面试问题

(1)ArrayList 和 LinkedList 有何不同?

答:先从底层数据结构开始说起,然后以某一个方法为突破口深入,比如:最大的不同是两者底层的数据结构不同,ArrayList 底层是数组,LinkedList 底层是双向链表,两者的数据结构不同也导致了操作的 API 实现有所差异,拿新增实现来说,ArrayList 会先计算并决定是否扩容,然后把新增的数据直接赋值到数组上,而 LinkedList 仅仅只需要改变插入节点和其前后节点的指向位置关系即可。

(2)ArrayList 和 LinkedList 应用场景有何不同

答:ArrayList 更适合于快速的查找匹配,不适合频繁新增删除,像工作中经常会对元素进行匹 配查询的场景比较合适,LinkedList 更适合于经常新增和删除,对查询反而很少的场景。

(3)ArrayList 和 LinkedList 两者有没有最大容量

答:ArrayList 有最大容量的,为 Integer 的最大值,LinkedList 底层是双向链表,理论上可以无限大,但源码中,LinkedList 实际大小用 的是 int 类型,这也说明了 LinkedList 不能超过 Integer 的最大值,不然会溢出。

(4)ArrayList 和 LinkedList 是如何对 null 值进行处理的

答:ArrayList 允许 null 值新增,也允许 null 值删除。删除 null 值时,是从头开始,找到第一个值是 null 的元素删除,LinkedList 也是允许null值的新增和删除的。

(5)ArrayList 和 LinedList 是线程安全的么,为什么?

答:当两者作为非共享变量时,比如说仅仅是在方法里面的局部变量时,是没有线程安全问题 的,只有当两者是共享变量时,才会有线程安全问题。

发布了43 篇原创文章 · 获赞 6 · 访问量 3907

猜你喜欢

转载自blog.csdn.net/weixin_44424668/article/details/104241279
今日推荐