一个问题让我直接闭门思过!!!拼多多面试必问项之List实现类:LinkedList

一、LinkedList概述

1、对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高。

2、LinkedList是一个实现了List接口和Deque接口的双端链表。

image

3、LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有 List 的操作以及双端队列和栈的性质。

4、LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:


List list=Collections.synchronizedList(``new` `LinkedList(...));

5、LinkedList 的继承结构如下:

image

二、实现的接口

源码:

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

(1)实现了 List 接口,具有List的操作方法;

(2)实现了 Deque 接口,具有双端队列和栈的特性;

(3)实现了 Cloneable 接口,支持克隆;

(4)实现了 Serializable 接口,支持序列化;

三、内部结构

1、双向链表

ArrayList是通过数组实现存储,而LinkedList则是通过链表来存储数据,而且他实现的是一个双向链表,简单的说一下什么是双向链表。

双向链表是数据结构的一种形式,他的每个节点维护两个指针,prev指向上一个节点,next指向下一个节点。

这种结构有什么特点呢?他可以实现双向遍历,这使得在链表中的数据读取变得非常灵活自由。

同时,LinkedList中维护了两个指针,一个指向头部,一个指向尾部。维护这两个指针后,可以使得元素从头部插入,也可以使元素从尾部插入。

基于方式,用户很容易就能实现FIFO(队列),LIFO(栈)等效果。那么下面我们来看一下源码中的具体实现。

2、内部结构分析

如图所示:

**image

image**

LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基

本结构。

Node除了保存数据,还定义了两个变量:

① prev变量记录前一个元素的位置

② next变量记录下一个元素的位置

看完了图之后,我们再看LinkedList类中的一个内部私有类Node就很好理解了:

 1     private static class Node<E> { 
2         E item;         //本节点的值3         Node<E> next;   //后继节点 
4         Node<E> prev;   //前驱节点 
5 6         Node(Node<E> prev, E element, Node<E> next) { 
7             this.item = element;8             this.next = next; 
9             this.prev = prev; 10 } 
11     }

这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。

四、成员变量

在 LinkedList 类中,还有几个成员变量如下:

1 // list 的长度2 transient int size = 0;3 4 // 链表头结点5 transient Node<E> first;6 7 // 链表尾结点8 transient Node<E> last; 
9 
10 //序列化标记
11 private static final long serialVersionUID = 876323262645176354L;

五、构造器

LinkedList 有两个构造器,如下:

(1)无参构造:

1     public LinkedList() { 
2     }

2)用已有的集合创建链表的构造方法:

1     public LinkedList(Collection<? extends E> c) { 
2         this(); 
3 addAll(c); 
4     }

注意:由于链表的容量可以一直增加,因此没有指定容量的构造器。

第一个为无参构造器。

第二个为使用指定集合的集合构造,并调用 addAll() 方法,继续跟进该方法,代码如下:

 1 public boolean addAll(Collection<? extends E> c) {2     return addAll(size, c); 3 }4     5 public boolean addAll(int index, Collection<? extends E> c) { 6     //1:检查index范围是否在size之内7     checkPositionIndex(index);8     9     //2:toArray()方法把集合的数据存到对象数组中
10     Object[] a = c.toArray(); 
11     int numNew = a.length; 
12     if (numNew == 0) 
13         return false; 
14         
15         
16     //3:获取当前链表的前驱和后继结点,得到插入位置的前驱节点和后继节点
17     Node<E> pred, succ; 
18     //如果插入位置为尾部,前驱节点为last,后继节点为null
19     if (index == size) { 
20         succ = null; 
21         pred = last; 
22 } 
23     //若非尾结点,获取指定位置的结点,调用node()方法得到后继节点,再得到前驱节点,
24     else { 
25         succ = node(index);  //获取当前节点
26         pred = succ.prev;    //获取当前节点前驱节点
27 } 
28     
29     // 4:循环将数组中的元素插入到链表
30     for (Object o : a) { 
31         @SuppressWarnings("unchecked") E e = (E) o; 
32         //创建新节点
33         Node<E> newNode = new Node<>(pred, e, null); 
34         //如果插入位置在链表头部
35         if (pred == null) 
36             first = newNode; 
37         else
38             pred.next = newNode; 
39         pred = newNode; 
40 } 
41     
42     //如果插入位置在尾部,重置last节点 
43     // 若插入到末尾,则数组中的最后一个元素就是尾结点
44     if (succ == null) { 
45         last = pred; 
46 } 
47     
48     //否则,将插入的链表与先前链表连接起来
49     else { 
50         // 若插入到指定位置,将数组中最后一个元素与下一个位置关联起来
51         pred.next = succ; 
52         succ.prev = pred; 
53 } 
54     size += numNew;
55     modCount++; 
56     return true; 
57 } 
58 
59 private void checkPositionIndex(int index) { 
60     if (!isPositionIndex(index)) 
61         throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 
62 } 
63 
64 private boolean isPositionIndex(int index) { 
65         return index >= 0 && index <= size; 
66 }

上面可以看出addAll方法通常包括下面四个步骤:

  1. 检查index范围是否在size之内;

  2. toArray()方法把集合的数据存到对象数组中;

  3. 得到插入位置的前驱和后继节点;

  4. 遍历数据,将数据插入到指定位置,如果没有在尾部,把原来数据链起来;

其中 node(index) 方法为获取指定位置的结点,代码如下:

 1     Node<E> node(int index) { 
2         // assert isElementIndex(index); 
3         // 判断下标在哪里,若下标在前一半,则从前往后遍历;否则从后往前遍历4         if (index < (size >> 1)) {5             Node<E> x = first;6             for (int i = 0; i < index; i++)7                 x = x.next; 
8             return x; 
9         } else { 
10             Node<E> x = last; 
11             for (int i = size - 1; i > index; i--) 
12                 x = x.prev; 
13             return x; 
14 } 
15     }

该方法通过遍历链表获取指定的元素。

值得注意的是,该方法并非直接从头到尾遍历整个链表,而是先判断下标的位置,若在前一半则从前往后遍历;否则就从后往前遍历。这样能减少遍历结点的个数。

因为链表的内存空间是非连续的,所以不支持随机访问(下标访问)。所以,查询某个结点是通过遍历整个链表来实现的。

六、常用方法

1、新增结点方法【尾插法】:add(),addLast(),offer(),offerLast()

源码分析:

 1     public boolean add(E e) { 2         linkLast(e);3         return true;4     }5     public void addLast(E e) { 6         linkLast(e);7     }8     public boolean offer(E e) {  9         return add(e); 
10 } 
11     public boolean offerLast(E e) { 
12 addLast(e); 
13         return true; 
14     }

可以看到他们都是调用了同一个方法 linkLast(e) 实现的,如下:

1     /**2      * Links e as last element.3      */4     void linkLast(E e) { 
5         final Node<E> l = last; 
6         // 创建一个节点,将prev指针指向链表的尾节点。7         final Node<E> newNode = new Node<>(l, e, null); 8 9         // 将last指针指向新创建的这个节点。 
10         last = newNode; 
11         
12         if (l == null) 
13             // 如果当前链表为空,那么将头指针也指向这个节点。
14             first = newNode; 
15 
16         else
17             // 若链表不为空,将新结点插入到链表尾部 
18             // 将链表的尾节点的next指针指向新建的节点,这样就完整的实现了在链表尾部添加一个元素的功能。
19             l.next = newNode; 
20         size++;21         modCount++; 
22     }

该操作就是将指定的结点添加到链表末尾。

2、新增节点【头插法】:addFirst(),offerFirst()

源码:

1     public void addFirst(E e) { 
2 linkFirst(e); 
3 }
4     public boolean offerFirst(E e) { 
5 addFirst(e); 
6         return true; 
7     }

可以看到他们都是调用了同一个方法 linkFirst(e) 实现的,如下:

1     /**2      * Links e as first element.3      */4     private void linkFirst(E e) { 
5         final Node<E> f = first; 
6         // 创建一个新元素,将元素的next指针指向当前的头结点7         final Node<E> newNode = new Node<>(null, e, f);8          // 将头指针指向这个节点。9         first = newNode; 
10         if (f == null) 
11             // 如果当前节点为空,则把尾指针指向这个节点。
12             last = newNode; 
13         else
14             // 将当前头结点的prev指针指向此结点。
15             f.prev = newNode; 
16         size++; 
17         modCount++; 
18     }

这段代码就是实现将元素添加的链表头部。

3、新增节点【指定位置插入】:add(int index, E element)

源码:

1     public void add(int index, E element) { 
2 checkPositionIndex(index); 
3 
4         if (index == size) 
5 linkLast(element); 
6         else
7 linkBefore(element, node(index)); 
8     }

在这里分了两种情况:

① 如果刚好到尾部,直接在尾部插入;

② 如果没有在尾部,在非null节点之前插入元素e。

源码:

 1     void linkLast(E e) { 
2         final Node<E> l = last; 
3         final Node<E> newNode = new Node<>(l, e, null);4         last = newNode; 
5         if (l == null)6             first = newNode; 
7         else8             l.next = newNode;9         size++; 
10         modCount++; 
11 } 
12     
13     void linkBefore(E e, Node<E> succ) { 
14         // assert succ != null;
15         final Node<E> pred = succ.prev; 
16         final Node<E> newNode = new Node<>(pred, e, succ); 
17         succ.prev = newNode; 
18         if (pred == null) 
19             first = newNode;20         else
21             pred.next = newNode;22         size++; 
23         modCount++; 
24     }

4、设置值:set(int index, E element)

源码:

 1     public E set(int index, E element) { 
2         //索引检查3         checkElementIndex(index);4         5         //获取该索引的元素6         Node<E> x = node(index); 
7         E oldVal = x.item; 
8         x.item = element; 
9         return oldVal; 
10     }

5、查找值:get(int index)

源码:

1     public E get(int index) { 
2         checkElementIndex(index);3         return node(index).item; 
4     }5     private void checkElementIndex(int index) { 
6         if (!isElementIndex(index))7             throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 
8     }9     private boolean isElementIndex(int index) { 
10         return index >= 0 && index < size; 
11     }

可以看到,这里还是调用了上面的 node() 方法进行查找的。

6、获取头节点:

源码:

 1     public E getFirst() { 
2         final Node<E> f = first; 
3         if (f == null)4             throw new NoSuchElementException(); 
5         return f.item; 
6     }7     public E element() { 
8         return getFirst(); 
9 }10     public E peek() { 
11         final Node<E> f = first; 
12         return (f == null) ? null : f.item; 
13 } 
14     public E peekFirst() { 
15         final Node<E> f = first; 
16         return (f == null) ? null : f.item; 
17     }

区别:

getFirst(),element(),peek(),peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,

其中getFirst()element() 方法将会在链表为空时,抛出异常;element()方法的内部就是使用getFirst()实现的。它们会在链表为空时,抛出NoSuchElementException;

而 peek() 和 peekFirst() 方法在链表为空时会返回空;

7、获取尾结点

1     public E getLast() {
2         final Node<E> l = last; 
3         if (l == null)4             throw new NoSuchElementException();5         return l.item;6     }7     public E peekLast() { 8         final Node<E> l = last; 9         return (l == null) ? null : l.item; 10     }

两者区别: getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只是会返回 null

8、根据对象得到索引的方法

int indexOf(Object o): 从头遍历找

 1     public int indexOf(Object o) { 
2         int index = 0;3         if (o == null) {4             //从头遍历5             for (Node<E> x = first; x != null; x = x.next) { 
6                 if (x.item == null)7                     return index;8                 index++;9 } 
10         } else {11             //从头遍历
12             for (Node<E> x = first; x != null; x = x.next) { 
13                 if (o.equals(x.item)) 
14                     return index; 
15                 index++; 
16 } 
17 } 
18         return -1;19     }

int lastIndexOf(Object o): 从尾遍历找

 1     public int lastIndexOf(Object o) { 
2         int index = size; 
3         if (o == null) {4             //从尾遍历5             for (Node<E> x = last; x != null; x = x.prev) { 
6                 index--;7                 if (x.item == null)8                     return index; 
9 } 
10         } else { 
11             //从尾遍历
12             for (Node<E> x = last; x != null; x = x.prev) { 
13                 index--; 
14                 if (o.equals(x.item))15                     return index; 
16 } 
17 }18         return -1; 
19     }

9、检查链表是否包含某对象的方法:contains()

源码:

1     public boolean contains(Object o) { 
2         return indexOf(o) != -1; 
3     }

10、删除头节点方法:remove() ,removeFirst(),pop(),poll(),pollFirst()

源码:

 1     public E remove() {2         return removeFirst();3     }4     5     public E pop() { 
6         return removeFirst();7     }8     9     public E removeFirst() { 
10         final Node<E> f = first; 
11         if (f == null) 
12             throw new NoSuchElementException(); 
13         return unlinkFirst(f); 
14 } 
15     
16     public E poll() { 
17         final Node<E> f = first; 
18         return (f == null) ? null : unlinkFirst(f); 
19 } 
20     public E pollFirst() {21         final Node<E> f = first; 
22         return (f == null) ? null : unlinkFirst(f); 
23     }

本质上都是调用了 unlinkFirst()方法

源码:

1     private E unlinkFirst(Node<E> f) {2         // assert f == first && f != null;3         final E element = f.item;4         final Node<E> next = f.next; 
5         f.item = null;6         f.next = null; // help GC7         first = next;8         if (next == null)9             last = null; 
10         else
11             next.prev = null; 
12         size--; 
13         modCount++; 
14         return element; 
15     }

11、删除尾节点方法:removeLast(),pollLast()

源码:

 1     public E removeLast() { 
2         final Node<E> l = last; 
3         if (l == null)4             throw new NoSuchElementException(); 
5         return unlinkLast(l); 
6     }7     8     public E pollLast() { 
9         final Node<E> l = last; 
10         return (l == null) ? null : unlinkLast(l); 
11     }

区别: removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null。

本质上都是调用了 unlinkLast()方法。

源码:

 1   private E unlinkLast(Node<E> l) { 
2         // assert l == last && l != null;3         final E element = l.item; 
4         final Node<E> prev = l.prev; 
5         l.item = null;6         l.prev = null; // help GC7         last = prev; 
8         if (prev == null)9             first = null; 
10         else
11             prev.next = null; 
12         size--;13         modCount++; 
14         return element; 
15     }

12、删除指定元素:remove(Object o) & 删除指定位置的元素:remove(int index)

 1     public boolean remove(Object o) { 
2         if (o == null) {3             for (Node<E> x = first; x != null; x = x.next) { 
4                 if (x.item == null) {5                     unlink(x);6                     return true;7                 }8             }9         } else { 
10             for (Node<E> x = first; x != null; x = x.next) { 
11                 if (o.equals(x.item)) {12 unlink(x);13                     return true; 
14 } 
15 } 
16 }17         return false;18 } 
19     
20     public E remove(int index) { 
21 checkElementIndex(index); 
22         return unlink(node(index)); 
23     }

当删除指定对象时,只需调用remove(Object o)即可,不过该方法一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。

本质上还是调用了 unlink(Node x) 方法:

 1   E unlink(Node<E> x) {2         // assert x != null;3         final E element = x.item;4         final Node<E> next = x.next;//得到后继节点5         final Node<E> prev = x.prev;//得到前驱节点6 7         //删除前驱指针8         if (prev == null) {9             first = next;//如果删除的节点是头节点,令头节点指向该节点的后继节点
10         } else { 
11             prev.next = next;//将前驱节点的后继节点指向后继节点
12             x.prev = null; 
13 } 
14 
15         //删除后继指针
16         if (next == null) { 
17             last = prev;//如果删除的节点是尾节点,令尾节点指向该节点的前驱节点
18         } else { 
19             next.prev = prev; 
20             x.next = null; 
21 } 
22 
23         x.item = null; 
24         size--; 
25         modCount++; 
26         return element; 
27     }

13、序列化方法:writeObject(java.io.ObjectOutputStream s)

源码:

1 private void writeObject(java.io.ObjectOutputStream s)2     throws java.io.IOException { 
3     // Write out any hidden serialization magic4     s.defaultWriteObject();5 6     // Write out size7     s.writeInt(size);8 9     // Write out all elements in the proper order.
10     for (Node<E> x = first; x != null; x = x.next) 
11 s.writeObject(x.item); 
12 }

14、反序列化方法:readObject(java.io.ObjectInputStream s)

源码:

1 private void readObject(java.io.ObjectInputStream s) 
2     throws java.io.IOException, ClassNotFoundException { 
3     // Read in any hidden serialization magic4     s.defaultReadObject();5 6     // Read in size7     int size = s.readInt();8 9     // Read in all elements in the proper order.
10     for (int i = 0; i < size; i++) 
11 linkLast((E)s.readObject()); 
12 }

七、作为其他数据结构

1、FIFO(队列)实现原理

队列的原理就是每次都从链表尾部添加元素,从链表头部获取元素,就像生活中的排队叫号,总是有个先来后到。

源码:

 1 // 队列尾部添加一个元素,建议使用这个,约定俗成吧。2 publicboolean offer(E e){3     return add(e); 4 }5 6 // 队列尾部添加一个元素7 publicboolean offerLast(E e){8     addLast(e);9     return    true; 
10 } 
11 
12 // offer和offerLast底层调用的都是linkLast这个方法,顾名思义就是将元素添加到链表尾部。
13 void linkLast(E e){ 
14     finalNode<E> l =last; 
15     
16     // 创建一个节点,将prev指针指向链表的尾节点。
17     finalNode<E> newNode =newNode<>(l, e,null); 
18     
19     // 将last指针指向新创建的这个节点。
20     last= newNode; 
21     
22     if(l ==null) 
23         // 如果当前链表为空,那么将头指针也指向这个节点。
24         first = newNode; 
25     else
26         // 将链表的尾节点的next指针指向新建的节点,这样就完整的实现了在链表尾部添加一个元素的功能。
27         l.next= newNode; 
28         
29     size++; 
30     modCount++; 
31 } 
32 
33 // 在链表头部删除一个元素,建议用这个
34 public E poll(){ 
35     final    Node<E> f = first; 
36     return(f ==null)?null: unlinkFirst(f); 
37 } 
38 // 在链表头部删除一个元素
39 public E pollFirst(){ 
40     final    Node<E> f = first; 
41     return(f ==null)?null: unlinkFirst(f); 
42 } 
43 
44 // poll和pollFirst底层调用的就是这个方法,将链表的头元素删除。
45 private E unlinkFirst(Node<E> f){ 
46     // assert f == first && f != null;
47     final E element = f.item; 
48     final    Node<E>next= f.next; 
49     f.item =null; 
50     f.next=null;// help GC
51     first =next; 
52     if(next==null) 
53         last=null; 
54     else
55         next.prev =null; 
56     size--; 
57     modCount++; 
58     return element; 
59 } 
60 
61 // 获取头元素,但是不会删除他。
62 public E peek(){ 
63     final    Node<E> f = first; 
64     return(f ==null)?null: f.item; 
65 }

更准确来说,链表是一个双端链表的结构,可以在头尾都进行操作节点。

2、LIFO(栈)实现原理:

栈的原理是每次从头部添加元素,也从头部获取元素,那么后进入的元素反而最先出来。就像我们平时叠盘子,洗好了就一个一个往上放,然后要用了就从上往下一个一个拿。

源码:

 1 // 在链表的头部添加一个元素2 publicvoid push(E e){3     addFirst(e);4 }5 6 // addFirst调用的就是linkFirst,这段代码就是实现将元素添加的链表头部。7 private void linkFirst(E e){8     final    Node<E> f = first; 
9     // 创建一个新元素,将元素的next指针指向当前的头结点
10     final    Node<E> newNode =newNode<>(null, e, f); 
11     // 将头指针指向这个节点。
12     first = newNode; 
13     if(f ==null) 
14         // 如果当前节点为空,则把尾指针指向这个节点。
15         last= newNode; 
16     else
17         // 将当前头结点的prev指针指向此结点。
18         f.prev = newNode; 
19     size++; 
20     modCount++; 
21 } 
22 
23 // 弹出顶部结点。
24 public E pop(){ 
25     return removeFirst(); 
26 }
27 
28 // removeFirst调用的就是unlinkFirst,unlinkFirst实现将链表顶部元素删除
29 private E unlinkFirst(Node<E> f){ 
30     // assert f == first && f != null;
31     final E element = f.item; 
32     final Node<E>next= f.next; 
33     f.item =null; 
34     f.next=null;// help GC
35     first =next; 
36     if(next==null) 
37         last=null; 
38     else
39         next.prev =null; 
40     size--; 
41     modCount++; 
42     return element; 
43 } 
44 
45 // 获取顶部结点,但是不删除
46 public E peek(){ 
47     final    Node<E> f = first; 
48     return(f ==null)?null: f.item; 
49 }

八、迭代器相关

LinkedList的迭代器实现有两个,一个是实现了Iterator接口的DescendingIterator,另一个则是实现了ListIterator接口的ListItr。

1、ListItr

源码:

1 public ListIterator<E> listIterator(int index) { 
2     checkPositionIndex(index);3     return new ListItr(index); 
4 }5 6 private class ListItr implements ListIterator<E> { 
7     private Node<E> lastReturned; 
8     private Node<E> next; 
9     private int nextIndex; 
10     private int expectedModCount = modCount;11 12     // 实例化的时候,将next指针指向指定位置的元素13     ListItr(int index) { 
14         // assert isPositionIndex(index);15         next = (index == size) ? null : node(index); 
16         nextIndex = index; 
17     }18 19     public boolean hasNext() { 
20         return nextIndex < size; 
21     }22 23     // 向后遍历24     public E next() { 
25         checkForComodification();26         if (!hasNext())27             throw new NoSuchElementException(); 
28 29         lastReturned = next; 
30         next = next.next; 
31         nextIndex++;32         return lastReturned.item; 33     }34 35     public boolean hasPrevious() { 36         return nextIndex > 0;37     }38 39     // 向前遍历40     public E previous() { 41         checkForComodification();42         if (!hasPrevious())43             throw new NoSuchElementException(); 44 45         lastReturned = next = (next == null) ? last : next.prev; 46         nextIndex--;47         return lastReturned.item; 48     }49 50     public int nextIndex() { 51         return nextIndex; 52     }53 54     public int previousIndex() {55         return nextIndex - 1;56     }57 58     public void remove() { 59         checkForComodification();60         if (lastReturned == null)61             throw new IllegalStateException(); 62 63         Node<E> lastNext = lastReturned.next; 64         unlink(lastReturned);65         if (next == lastReturned) 66             next = lastNext; 67         else68             nextIndex--;69         lastReturned = null;70         expectedModCount++;71     }72 73     public void set(E e) { 74         if (lastReturned == null)75             throw new IllegalStateException(); 76         checkForComodification();77         lastReturned.item = e; 78     }79 80     public void add(E e) { 81         checkForComodification();82         lastReturned = null;83         if (next == null)84             linkLast(e);85         else86             linkBefore(e, next);87         nextIndex++;88         expectedModCount++;89     }90 91     public void forEachRemaining(Consumer<? super E> action) { 92         Objects.requireNonNull(action);93         while (modCount == expectedModCount && nextIndex < size) { 94             action.accept(next.item);95             lastReturned = next; 96             next = next.next; 97             nextIndex++;98         }99 checkForComodification(); 100 } 101 
102     final void checkForComodification() { 
103         if (modCount != expectedModCount) 
104             throw new ConcurrentModificationException(); 
105 } 
106 }

2、DescendingIterator

DescendingIterator迭代器实现的是对链表从尾部向头部遍历的功能,他复用里ListItr中的previous方法,将当前位置指向链表尾部,然后逐个向前遍历。

源码:

1     private class DescendingIterator implements Iterator<E> { 
2         private final ListItr itr = new ListItr(size()); 
3         public boolean hasNext() { 
4             return itr.hasPrevious(); 
5         }6         public E next() { 7             return itr.previous(); 8         }9         public void remove() { 10 itr.remove(); 11 } 12     }

九、不同版本的 LinkedList

在LinkedList 中 JDK1.6 之前为双向循环链表,JDK1.7 取消了循环,采用双向链表。

1、双向链表

双向链表属于链表的一种,也叫双链表双向即是说它的链接方向是双向的,它由若干个节点组成,每个节点都包含下一个节点和上一个节点的指针,所以从双向链表的任意节点开始,都能很方便访问他的前驱结点和后继节点。

image

2、双向链表特点

    • 创建双链表时无需指定链表的长度。
    • 比起单链表,双链表需要多一个指针用于指向前驱节点,所以需要存储空间比单链表多一点。
    • 双链表的插入和删除需要同时维护 next 和 prev 两个指针。
    • 双链表中的元素访问需要通过顺序访问,即要通过遍历的方式来寻找元素。

3、双向循环链表

前面的双向链表的 head 节点和链尾没有连接关系,所以如果要访问最后一个节点的话需要从头开始遍历,直到最后一个节点。在双向链表基础上改进一下,把 header 节点的 prev 指针指向最后一个节点,而最后一个节点的 next 指针指向 header 节点,于是便构成双向循环链表。

image

image

更多链表操作:https://juejin.cn/post/6844903648154271757#heading-0

4、JDK6

在JDK 1.7之前(此处使用JDK1.6来举例),LinkedList是通过headerEntry实现的一个循环链表的。先初始化一个空的Entry,用来做header,然后首尾相连,形成一个循环链表:

1 privatetransient Entry<E>header =new Entry<E>(null,null,null); 
2 
3 public LinkedList() {header.next =header.previous =header; }

image

在LinkedList中提供了两个基本属性size、header。

1 private transient Entry<E> header = new Entry<E>(null, null, null); 
2 private transient int size = 0;
  • 其中size表示的LinkedList的大小,header表示链表的表头,Entry为节点对象。
1 private static class Entry<E> { 
2         E element;        //元素节点3         Entry<E> next;    //下一个元素4         Entry<E> previous;  //上一个元素5  6         Entry(E element, Entry<E> next, Entry<E> previous) {7             this.element = element; 8             this.next = next; 9             this.previous = previous;10 } 11 }
  • 上面为Entry对象的源代码,Entry为LinkedList的内部类,它定义了存储的元素。该元素的前一个元素、后一个元素,这是典型的双向链表定义方式。

每次添加/删除元素都是默认在链尾操作。对应此处,就是在header前面操作,因为遍历是next方向的,所以在header前面操作,就相当于在链表尾操作。

如下面的插入操作addBefore以及图示,如果插入obj_3,只需要修改header.previous和obj_2.next指向obj_3即可。`

image

1 private Entry<E> addBefore(E e, Entry<E> entry) { 
2         //利用Entry构造函数构建一个新节点 newEntry,3         Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);4         //修改newEntry的前后节点的引用,确保其链表的引用关系是正确的5         newEntry.previous.next = newEntry; 6         newEntry.next.previous = newEntry; 7         //容量+18         size++;9         //修改次数+1
10         modCount++; 
11         return newEntry; 
12     }
  • 在addBefore方法中无非就是做了这件事:构建一个新节点newEntry,然后修改其前后的引用。

5、JDK7

在JDK 1.7,1.6的headerEntry循环链表被替换成了first和last组成的非循环链表。

 1     transient int size = 0;2  3     /**4      * Pointer to first node.5      * Invariant: (first == null && last == null) ||6      *            (first.prev == null && first.item != null)7      */8     transient Node<E> first; 9  
10     /**
11 * Pointer to last node. 
12 * Invariant: (first == null && last == null) || 
13 *            (last.next == null && last.item != null) 
14      */
15     transient Node<E> last;

[

  • 在初始化的时候,不用去new一个Entry。

image


1     /**
2 * Constructs an empty list. 
3      */
4     public LinkedList() { 
5     }
  • 在插入/删除的时候,也是默认在链尾操作。把插入的obj当成newLast,挂在oldLast的后面。另外还要先判断first是否为空,如果为空则first = obj。

image

如下面的插入方法linkLast,在尾部操作,只需要把obj_3.next指向obj_4即可。

1     void linkLast(E e) { 
2         final Node<E> l = last; 
3         final Node<E> newNode = new Node<>(l, e, null);4         last = newNode; 5         if (l == null)6             first = newNode; 7         else8             l.next = newNode; 9         size++; 10         modCount++; 11     }

其中

1  private static class Node<E> { 
2           E item;3           Node<E> next; 4           Node<E> prev; 5   6           Node(Node<E> prev, E element, Node<E> next) { 7               this.item = element; 8               this.next = next; 9               this.prev = prev; 10 } 11      }

6、【1.6-header循环链表】 V.S 【1.7-first/last非循环链表】

JDK 1.7中的first/last对比以前的header有下面几个好处:

  • (1) first / last有更清晰的链头、链尾概念,代码看起来更容易明白。

  • (2)first / last方式能节省new一个headerEntry。(实例化headerEntry是为了让后面的方法更加统一,否则会多很多header的空校验)

  • (3)在链头/尾进行插入/删除操作,first /last方式更加快捷。

插入/删除操作按照位置,分为两种情况:中间 和 两头。

在中间插入/删除,两者都是一样,先遍历找到index,然后修改链表index处两头的指针。
    在两头,对于循环链表来说,由于首尾相连,还是需要处理两头的指针。而非循环链表只需要处理一边first.previous/last.next,所以理论上非循环链表更高效。      恰恰在两头(链头/链尾) 操作是最普遍的

(对于遍历来说,两者都是链表指针循环,所以遍历效率是一样的。)

十、线程安全性

线程安全的概念不再赘述。分析以下场景:

若有线程 T1 对 LinkedList 进行遍历,同时线程 T2 对其进行结构性修改。

对 LinkedList 的遍历是通过 listIterator(index) 方法实现的,如下:

1     public ListIterator<E> listIterator(int index) { 
2         checkPositionIndex(index);3         return new ListItr(index); 4     }5 6 7     private class ListItr implements ListIterator<E> { 8         private Node<E> lastReturned; 9         private Node<E> next; 10         private int nextIndex; 11         // 初始化时二者是相等的
12         private int expectedModCount = modCount; 
13 
14 
15         ListItr(int index) { 
16             // assert isPositionIndex(index);
17             next = (index == size) ? null : node(index); 
18             nextIndex = index; 
19 } 
20 
21 
22         public E next() { 
23 checkForComodification(); 
24             if (!hasNext()) 
25                 throw new NoSuchElementException();26 
27 
28             lastReturned = next; 
29             next = next.next; 
30             nextIndex++; 
31             return lastReturned.item; 
32 } 
33 
34 
35         public void remove() { 
36 checkForComodification(); 
37             if (lastReturned == null) 
38                 throw new IllegalStateException(); 
39 
40 
41             Node<E> lastNext = lastReturned.next; 
42 unlink(lastReturned); 
43             if (next == lastReturned) 
44                 next = lastNext; 
45             else
46                 nextIndex--; 
47             lastReturned = null; 
48             expectedModCount++; 
49 } 
50 
51 
52         // ... 
53         
54         // 是否有其他线程对当前对象进行结构修改
55         final void checkForComodification() { 
56             if (modCount != expectedModCount) 
57                 throw new ConcurrentModificationException(); 
58 } 
59 }

该类的 next(), add(e) 等方法在执行时会检测 modCount 与创建时是否一致(checkForComodification() 方法),从而判断是否有其他线程对该对象进行了结构修改,若有则抛出 ConcurrentModificationException 异常。

因此,LinkedList 是线程不安全的。

十一、总结

1、LinkedList 内部是【双向链表】,同时实现了 List 接口 和 Deque 接口,因此也具备 List、双端队列和栈的性质。

2、线程不安全。
最后,祝大家早日学有所成,拿到满意offer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/299561.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker小白到实战之开篇概述

前言“不对啊&#xff0c;在我这运行很正常啊”&#xff0c;这句话小伙伴们在前几年应该听得很多&#xff1b;每次一到安装、部署时总有一堆问题&#xff0c;毕竟操作系统版本、软件环境、硬件资源、网络等因素在作怪&#xff0c;此时难免会导致开发小伙伴和运维哥们互相甩锅&a…

设置su为不需要密码切换为root

设置su为不需要密码 如果需要对某用户su命令也不需要输入密码&#xff0c;则需要修改下列的&#xff1a;1--->如果没有wheel组 则用sudo groupadd wheel创建命令为 sudo groupadd wheel&#xff1b;2---->sudo vim /etc/group将username和root加入到wheel用户组内 如图&a…

被女朋友拉黑后,我写了个“舔狗”必备神器

全世界只有3.14 % 的人关注了 青少年数学之旅 “ 在一个阳光明媚的清晨&#xff0c;我打开窗户呼吸了一口新鲜空气。阳光灿烂&#xff0c;岁月静好&#xff0c;又是一个约女朋友出去爬山吃饭看电影的好日子。 图片来自包图网 想到女朋友的大眼睛&#xff0c;我脸上不禁洋溢起了…

涨薪关键之反射机制,引得项目经理对你的看重,加薪触手可及!!!!

前言 就比如我前几天被面试官问什么是反射&#xff1f;&#xff1f;&#xff1f; 而我的回答是&#xff01;&#xff01;&#xff01; 反射是动态语言的关键&#xff0c;反射允许程序在执行期间借助Reflection API取得任何类的内部信息&#xff0c;并能直接操作任曦对象的内…

如何摆脱「自我否定」状态

大家好&#xff0c;我是Z哥。你最近正处于自我否定的状态吗&#xff1f;如果不是的话&#xff0c;回想一下最近的一次处于这种状态是什么时候&#xff1f;当时的感受如何&#xff1f;以及&#xff0c;最终是如何走出这个状态的&#xff1f;不着急&#xff0c;给你 1 分钟回忆一…

struct and union

[url]http://hi.baidu.com/tweigh/blog/item/5303d2ef6e2720eace1b3e9d.html[/url]1. struct的巨大作用面对一个人的大型C/C程序时&#xff0c;只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。因为一个大型的C/C程序&#xff0c;势必要 涉及一些(甚至大量)进…

编码GBK的不可映射字符

为什么80%的码农都做不了架构师&#xff1f;>>> 由于JDK是国际版的&#xff0c;在编译的时候&#xff0c;如果我们没有用-encoding参数指定我们的JAVA源程序的编码格式&#xff0c; 则javac.exe首先获得我们操作系统默认采用的编码格式&#xff0c; 也即在编译java…

低调的大神!他改变了半导体产业!史上唯一两次获得诺贝尔物理奖,却几乎被人遗忘...

全世界只有3.14 % 的人关注了青少年数学之旅两次获得诺贝尔奖的科学家&#xff0c;世界上仅有这四个人&#xff01;他们是&#xff1a;1. 居里夫人(Marie Curie,1867~1934),波兰科学家,他的丈夫叫皮埃尔居里,两人合称“居里夫妇”! 1903年,居里夫妇和亨利...2.约翰巴丁 美国物理…

入职第一天,我接手了号称【屎山】的祖传代码,这还能卷吗???

公司各种各样的祖传代码都是令新人虎躯一震的代码&#xff0c;因为有时候你根本不知道它是干嘛的&#xff0c;甚至觉得它毫无用处&#xff0c;关键是 还绝对不能动&#xff0c;碰一段改半年&#xff0c;别问我怎么知道的。最讽刺的是&#xff0c;你可能为了修改代码&#xff0c…

设计模式之迭代器

迭代器模式介绍集合的结构迭代器模式是一种行为设计模式&#xff0c;让你能在不暴露集合底层表现形式(列表、栈、树等)的情况下遍历集合中所有的元素。迭代器模式满足了单一职责和开闭原则&#xff0c;外界的调用方也不需要知道任何一个不同的数据结构在使用上的遍历差异。迭代…

倒啤酒竟能拿到诺贝尔物理学大奖!明明是普通操作,凭什么这么强?

全世界只有3.14 % 的人关注了青少年数学之旅倒啤酒&#xff0c;是艺术&#xff0c;更是一门学问。每个初来社会报道的男孩&#xff0c;都必须掌握这个酷炫技能。它是从古自今的文化传承&#xff0c;也是一个男人成熟的标志。向快手老铁致敬然而&#xff0c;理想很丰满&#xff…

铁山靠之——HarmonyOS基础 - 1.0

HarmonyOS学习第一章 一、HarmonyOS简介1.1 安装和使用DevEco Studio1.2 环境配置1.3 项目创建1.4 运行程序1.5 基本工程目录1.5.1 工程级目录1.5.2 模块级目录1.5.3 app.json51.5.4 module.json51.5.5 main_pages.json 二、TypeScript快速入门2.1 简介2.2 基础类型2.2.1 布尔值…

vb.net2.0 Hmac-md5加密算法

2019独角兽企业重金招聘Python工程师标准>>> <summary> Hmac-md5加密算法。 </summary> <param name"APassWord">加密串(即密码)</param> <param name"ASourceStr">原始字符串</param> <returns>&l…

解决VS2010 beta2 安装后html标签和script智能提示不起作用的问题

此文已经移至&#xff1a;解决VS2010 beta2 安装后html标签和script智能提示不起作用的问题 安装VS2010 beta2 后&#xff0c;发现正常的html标签的智能提示不起作用&#xff0c;比如<a>,<p>&#xff0c;还有在<script>里面也没有了js的智能提示。 解决办法…

初级Java开发面试必问项!!! 标识符、字面值、变量、数据类型,该学学了!

最近事情太多&#xff0c;没太时间写博客。今天抽空再整理整理面试中的那点事吧&#xff0c;帮助那些正在找工作或想跳槽找工作的学弟学妹们。 前面我己写过多篇推文&#xff0c;相信看过我文章的伙伴们已经了解掌握了不少。从目前流行的开发技术、常见的面试问题以及问题的答…

你还记得当初是怎么对我的吗? | 今日最佳

全世界只有3.14 % 的人关注了青少年数学之旅&#xff08;图源阿粪青&#xff0c;侵权删&#xff09;

4项技巧使你不再为PHP中文编码苦恼

2019独角兽企业重金招聘Python工程师标准>>> PHP程序设计中中文编码问题曾经困扰很多人&#xff0c;导致这个问题的原因其实很简单&#xff0c;每个国家(或区域)都规定了计算机信息交换用的字符编码集&#xff0c;如美国的扩展 ASCII 码&#xff0c;中国的 GB2312-8…

Hello Blazor:(9)Source Generators生成导航菜单

前言最近写了多篇关于Source Generators的文章&#xff0c;发现它确实可以简化我们的部分开发工作。这不&#xff0c;我又盯上了Blazor。问题默认的NavMenu.razor组件用于显示导航菜单&#xff0c;它的部分代码如下&#xff1a;<div class"NavMenuCssClass" oncli…

你绝对没想过原来数学家这么流氓,一言不合就进行暴力证明

全世界只有3.14 % 的人关注了青少年数学之旅1最经典的“无字证明”1989 年的《美国数学月刊》&#xff08;American Mathematical Monthly&#xff09;上有一个貌似非常困难的数学问题&#xff1a;下图是由一个个小三角形组成的正六边形棋盘&#xff0c;现在请你用右边的三种&a…

面试阿里被问到JVM,不逼逼赖赖,直接盘给面试官看!!!

面试阿里被问到JVM&#xff0c;不逼逼赖赖&#xff0c;直接盘给面试官看&#xff01;&#xff01;&#xff01;概述JVM体系结构类加载机制类加载器类加载过程双亲委派机制全盘负责委托机制打破双亲委派机制自定义类加载器实现JVM运行时数据区程序计数器虚拟机栈本地方法栈堆方法…