【源码系列】Java中的数据结构——栈,队列,链表与LinkedList2

简介: 【源码系列】Java中的数据结构——栈,队列,链表与LinkedList

3.List接口的常用方法

①add(E e)

public boolean add(E e) {
        linkLast(e);
        return true;
    }

点开linkLast方法看看,


//向尾部增加一个元素
void linkLast(E e) {
  //最后一个结点
    final Node<E> l = last;
    //创建一个新的结点对象
    final Node<E> newNode = new Node<>(l, e, null);
    //更新last指针为新的
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    //元素数量自增1
    size++;
    //修改的数量
    modCount++;
}


可以看到这个方法就是向尾部增加一个新的结点


②remove(Object o)

//遍历所有结点,如果是该对象则删除结点,返回true;否则返回false表示未找到
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;
}


我们看看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指针指向下一个即可
            first = next;
        } else {
          //如果不是则将前一个结点指向后一个结点
            prev.next = next;
            x.prev = null;
        }
  //判断是不是尾结点
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
  //将其值改为null,其实按理来说只需指针丢弃即可在逻辑上将结点从链表上删除,但这里可能是想让拥有这个结点指针的程序知道这个结点已经被删除了,同时把结点的值对象指针丢弃也利于gc回收
        x.item = null;
        //更新size
        size--;
        //修改计数+1
        modCount++;
        return element;
    }


这里有个有意思的地方,其实按理来说只需指针丢弃即可在逻辑上将结点从链表上删除,但这里却有个x.item=null的操作,我猜测作者可能是想让拥有这个结点指针的程序知道这个结点已经被删除了,还有就是把这个结点的对象指针丢弃可以利于gc回收(毕竟有指针指着,gc也不会强行回收掉)。


③remove(int index)

public E remove(int index) {
  //边界检查,就是看看index合不合法
        checkElementIndex(index);
        return unlink(node(index));
    }

这里我们可以看看node(int index)方法,


Node<E> node(int index) {

Node<E> node(int index) {
//把index下标与size的一半去比较,言外之意就是比较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;
        }
    }

这个方法就是根据下标去寻找对应的元素,不过呢,这里有点小优化,他会判断这个下标是前半部分还是后半部分来选择从前往后遍历还是从后往前遍历。然后把对应的几点返回。


④set(int index, E element)

public E set(int index, E element) {
  //下标检查
        checkElementIndex(index);
        //查找对应的结点
        Node<E> x = node(index);
        E oldVal = x.item;
        //更新值
        x.item = element;
        //返回旧值
        return oldVal;
}


这个方法就是根据下标对对应结点值进行修改。


⑤indexOf(Object o)

public int indexOf(Object o) {
        int index = 0;
        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;


⑥lastIndexOf(Object o)

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


这个方法和上面方法类似,都是返回值相同的结点下标,不过它是从后往前找,所以它会返回最后那个值相同的结点下标;若没找到则返回-1。


⑦clear()

public void clear() {
        // Clearing all of the links between nodes is "unnecessary", but:
        // - helps a generational GC if the discarded nodes inhabit
        //   more than one generation
        // - is sure to free memory even if there is a reachable Iterator
        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++;
    }


这里有一步奇怪的的操作,就是循环把每个结点里的结点指针和对象指针都设为null,源码里注释就是上面那句英文,大概意思是清除所有不需要的结点之间的联系,利于帮助gc回收。


你可能看起来很奇怪,我给你解释一下:

这步操作涉及gc的内存回收的过程,我们如何判断这块内存有没有用呢?很重要的依据就是有没有指针指向块内存。这里就是要把这种指针给消除了,把这种相互之间的联系去除了。具体的操作就是把这些无用的对象指针都变为null,这样gc就能更好的判定它是无用的,从而更早的回收掉这部分无用的内存。


4.Queue接口的常用方法

我们之前说了,队列实际上就是一个操作受限的线性表,用链表也可以实现一个队列。

事实上LinkedList就可以当做队列来操作,比如下面这样


Queue<Integer> queue=new LinkedList();
//入队
queue.add(1);
//打印队首元素
System.out.println(queue.peek());
//出队
queue.poll();


这里就是一个典型队列使用例子,Queue接口约束了其他方法,从而实现了队列那种先进先出的效果


①add(E e)

public boolean add(E e) {
  //创建一个值为e的结点,加到队尾
        linkLast(e);
        return true;
}


这个add方法实际上就是之前List接口里的add方法


②poll()

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

取出队首元素,实际上就是取出链表头部的元素。


③peek()

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


返回队首元素的值,实际上就是返回链表头部元素的值。


5.Deque接口的常用方法

Deque是个双向队列的接口,但实际上也是可以用来当做栈使用的。就像下面这样,


Deque<Integer> stack=new LinkedList();
//入栈
stack.push(1);
//输出栈顶元素
System.out.println(stack.peek());
//出栈
stack.pop();


①push(E e)

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

入栈,实际上就是往链表头部插入一个元素


②pop()

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

出栈,实际上也是删除返回链表头部元素


③peek()

返回栈顶元素,

这个和Queue中一样就不展示了。


6.LinkedList迭代器

注意:LinkedList没有Iterator的实现,调用Iterator()方法也是返回ListItr对象


①ListItr

private class ListItr implements ListIterator<E> {
        private Node<E> lastReturned;
        private Node<E> next;
        private int nextIndex;
        private int expectedModCount = modCount;
        ListItr(int index) {
            // assert isPositionIndex(index);
            next = (index == size) ? null : node(index);
            nextIndex = index;
        }
        public boolean hasNext() {
            return nextIndex < size;
        }
        public E next() {
          //检查是否存在预期外的修改
            checkForComodification();
            //判断next是否越界
            if (!hasNext())
                throw new NoSuchElementException();
    //指针后移
            lastReturned = next;
            next = next.next;
            //下标+1
            nextIndex++;
            return lastReturned.item;
        }
        public boolean hasPrevious() {
            return nextIndex > 0;
        }
        public E previous() {
            checkForComodification();
            if (!hasPrevious())
                throw new NoSuchElementException();
            lastReturned = next = (next == null) ? last : next.prev;
            nextIndex--;
            return lastReturned.item;
        }
        public int nextIndex() {
            return nextIndex;
        }
        public int previousIndex() {
            return nextIndex - 1;
        }
        public void remove() {
            checkForComodification();
            if (lastReturned == null)
                throw new IllegalStateException();
            Node<E> lastNext = lastReturned.next;
            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;
        }
        public void add(E e) {
            checkForComodification();
            lastReturned = null;
            if (next == null)
                linkLast(e);
            else
                linkBefore(e, next);
            nextIndex++;
            //预期修改增加
            expectedModCount++;
        }
        public void forEachRemaining(Consumer<? super E> action) {
          //检查action参数对象是否为null
            Objects.requireNonNull(action);
            //遍历执行action的accept方法,把结点的值作为参数传入
            while (modCount == expectedModCount && nextIndex < size) {
                action.accept(next.item);
                lastReturned = next;
                next = next.next;
                nextIndex++;
            }
            //检查是否有预期外的修改
            checkForComodification();
        }
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }



这个迭代器和ArrayList中的ListItr的套路如出一辙,唯一的区别就是LinkedList遍历是通过结点的指针不断往前。具体的可以看我前一篇【源码系列】Java中的数据结构——数组与ArrayList,里面也讲了为什么会有modcount以及迭代器是如何实现干扰检测的机制。当然这里的机制实现也有一点小的区别,在ArrayList中expectedModCount是通过expectedModCount = modCount;直接改变的,而这里是expectedModCount++。但这里我感觉区别不大,这里就不再赘述了。


当然,这里的forEachRemaining方法特别有意思,它可以支持我们用lambda表达式。从这里的源码可以看出lambda表达式的原理——看似写的是函数,实际上写的只是一个实现了对应接口的匿名内部类而已,这里的参数也并不是一个函数,而是一个接口的实现类。


②DescendingIterator 逆序迭代器

private class DescendingIterator implements Iterator<E> {
        private final ListItr itr = new ListItr(size());
        public boolean hasNext() {
            return itr.hasPrevious();
        }
        public E next() {
            return itr.previous();
        }
        public void remove() {
            itr.remove();
        }
    }


是不是很有意思,里面的实现还是ListItr,只不过next和它反着来(笑哭)。


③DescendingIterator 拆分迭代器

static final class LLSpliterator<E> implements Spliterator<E> {
        static final int BATCH_UNIT = 1 << 10;  // batch array size increment
        static final int MAX_BATCH = 1 << 25;  // max batch array size;
        final LinkedList<E> list; // null OK unless traversed
        Node<E> current;      // current node; null until initialized
        int est;              // size estimate; -1 until first needed
        int expectedModCount; // initialized when est set
        int batch;            // batch size for splits
        LLSpliterator(LinkedList<E> list, int est, int expectedModCount) {
            this.list = list;
            this.est = est;
            this.expectedModCount = expectedModCount;
        }
        final int getEst() {
            int s; // force initialization
            final LinkedList<E> lst;
            if ((s = est) < 0) {
                if ((lst = list) == null)
                    s = est = 0;
                else {
                    expectedModCount = lst.modCount;
                    current = lst.first;
                    s = est = lst.size;
                }
            }
            return s;
        }
        public long estimateSize() { return (long) getEst(); }
        public Spliterator<E> trySplit() {
            Node<E> p;
            int s = getEst();
            if (s > 1 && (p = current) != null) {
                int n = batch + BATCH_UNIT;
                if (n > s)
                    n = s;
                if (n > MAX_BATCH)
                    n = MAX_BATCH;
                Object[] a = new Object[n];
                int j = 0;
                do { a[j++] = p.item; } while ((p = p.next) != null && j < n);
                current = p;
                batch = j;
                est = s - j;
                return Spliterators.spliterator(a, 0, j, Spliterator.ORDERED);
            }
            return null;
        }
        public void forEachRemaining(Consumer<? super E> action) {
            Node<E> p; int n;
            if (action == null) throw new NullPointerException();
            if ((n = getEst()) > 0 && (p = current) != null) {
                current = null;
                est = 0;
                do {
                    E e = p.item;
                    p = p.next;
                    action.accept(e);
                } while (p != null && --n > 0);
            }
            if (list.modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
        public boolean tryAdvance(Consumer<? super E> action) {
            Node<E> p;
            if (action == null) throw new NullPointerException();
            if (getEst() > 0 && (p = current) != null) {
                --est;
                E e = p.item;
                current = p.next;
                action.accept(e);
                if (list.modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                return true;
            }
            return false;
        }
        public int characteristics() {
            return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
        }
    }

这个拆分迭代器时jdk8引入的,是为了满足流式编程的需要,用于并行流的拆分,这块我以后再补。(未完待续…)


三、总结

链表和数组一样都是线性表的一种,不过有别于数组的连续存储,它采取零散存储的方式,尽管它会造成更多的内存消耗,但是也会带来一些数组没有的好处。而LinkedList便是链表的实现和封装。


对于LinkedList你需要记住以下几点:


LinedList实际是由链表实现,是一种双向链表的实现。

LinkedList可以是栈、队列的实现,LinkedList实现了Queue,Deque(Queue的子接口)的接口,可以分别对应队列和栈的实现,能这样做的根本原因就在于队列和栈都是一种受限制的线性表。

LinkedList增删操作快,时间复杂度为O(1);但是根据下标查找元素慢,其时间复杂度为O(n)。

LinkedList查找下标元素时,会进行对下标进行判断,如果是前半部分就从头结点开始遍历查找,否则从尾结点开始遍历查找,这样做可以提升效率(思路有点像跳表)。

LinkedList适用于增删多,查询操作少的情况。

LinkedList迭代器中同样用modcount来进行干扰检测,提示集合迭代过程中程序出现的问题。

LinkedList在执行clear方法进行清除所有元素时,它会把所有元素结点的所有指针都赋值为null,这么做的目的就是使这些无用对象尽快地被gc回收。


相关文章
|
27天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
123 9
|
18天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
23 1
|
6天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
28 5
|
21天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
24天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
26天前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
47 4
|
1月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储 算法 Java
🧠Java零基础 - Java栈(Stack)详解
【10月更文挑战第17天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
25 2
|
2月前
数据结构(栈与列队)
数据结构(栈与列队)
20 1
|
2月前
【数据结构】-- 栈和队列
【数据结构】-- 栈和队列
17 0