1.介绍

  链表是数据结构中一种很重要的数据结构,一个链表含有一个或者多个节点,每个节点处理保存自己的信息之外还需要保存上一个节点以及下一个节点的指针信息。通过链表的表头就可以访问整个链表的信息。Java API中提供了链表的Java实现---LinkedList下。LinkedList是通过节点的连接实现链表的数据结构,向linkedList中插入或删除元素的速度是特别快,而随机访问的速度相对较慢,这个是由于链表本身的性质造成的,在链表中,每个节点都包含了前一个节点的引用,后一个节点的引用和节点存储值,当一个新节点插入式,只需要修改其中相关的前后关系节点引用即可,删除节点也是一样。操作对象只需要改变节点的链接,新节点可以存放在内存的任何位置,但也就是因为如此LinkedList虽然存在get()方法,但是这个方法通过遍历节点来定位所以速度很慢。LinkedList还需要单独addFrist(),addLast(),getFrist(),getLast(),removeFirst(),removeLast()方法,这些方法使得LinkedList可以作为堆栈,队列,和双队列来使用。下面是LinkedList使用的简单示例:

  

package com.test.collections;

import java.util.LinkedList;

public class LinkedListTest {

	/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
LinkedList<String> linkedList = new LinkedList<String>();
linkedList.add("A");
linkedList.add("B");
linkedList.add("C");
linkedList.add("D");
linkedList.add("E");
linkedList.add("F");
linkedList.addFirst("G");
linkedList.addLast("H");
System.out.println(linkedList.element());
System.out.println(linkedList.contains("A"));
System.out.println(linkedList.element());
System.out.println(linkedList.get(4));
System.out.println(linkedList.getFirst());
System.out.println(linkedList.getLast());
System.out.println(linkedList.indexOf("C"));
System.out.println(linkedList.contains("D"));
System.out.println(linkedList.offer("F"));
System.out.println(linkedList.isEmpty());
System.out.println(linkedList.iterator().next());
linkedList.push("N") ;
} }

 2.继承结构

  LinkedList继承了AbstractSequentialList类,实现了List<E>, Deque<E>, Cloneable, Serializable这几个接口,含有三个transient类型的属性size,Node<E>类型的first和last;first  指向了第一个节点指针,last指向了最后一个节点的指针。还需要说明的几点是ListedList的几个内部类,实现了ListIterator接口的ListItr类,保存节点信息的Node类,实现了Iterator<E接口的DescendingIterator类,提供了向下的迭代器实现。我们重点看下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;
}
}

  Node节点中item保存具体的对象的值,next节点指向下一个节点指针,prev指向了上一个节点的指针。

3.具体的源码解析

  a:构造函数

  

 public LinkedList() {
} public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false; Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
} for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
} if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
} size += numNew;
modCount++;
return true;
}

  第一个构造函数是构造了一个空的链表,第二个构造函数是通过集合构造了一个链表相当于直接初始化的过程。第二个过程比较复杂,我们做个简单的说明。其实第二个构造方法只需要把addAll()方法讲明白了就说名了这个构造函数的具体实现。在addAll()方法中首先判断传入的索引是否合法;然后将集合转化成我们需要的Object类型的数组(注意是数组);

   Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}

  这个判断很重要,如果索引和链表的大小一致说明插入的集合只能放到链表尾部,这个时候讲尾指针赋值给pred;如果不相等说明只能把集合插入到链表的中部的某一个位置。这个是需要获取这个节点的元素才行:

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

  通过这个方法获取到了插入节点的位置,并且将这个节点的前一个指针指向我们的元素节点pred;最后就开始了插入数组的过程。通过循环遍历这个数组不但的将数组的值插入到我们需要的位置,并且不断的更新链表的大小size属性。

b:get(int)

  

  public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

  获取某一个索引的对象的方法很简单首先判断索引是否越界合法,然后根据node()函数返回节点及其对应的值。

c:set(int,E)

   public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

  重置某一个节点的值,这个实现也很简单,就是首先检查索引是否越界合法,然后更加node()函数获取这个节点,然后更新这个节点对应的值即可。

d:add(int,E)

  public void add(int index, E element) {
checkPositionIndex(index); if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
} void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
} void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

  在链表的某一个节点增加一个新的节点的实现方法:首先检查输入的下标索引是否合法,然后通过判断是添加到链表的尾部还是链表的中部,如果是尾部的话就要调用linkLast(E)方法,如果是链表的中部就要使用 linkBefore(element, node(index));方法。

linkLast(E):直接插入到链表尾部修改一下链表尾部的指针即可还需要更新链表的长度。

linkBefore(E e, Node<E> succ):将一个节点插入到一个非空的元素前面,如果插入的是头结点位置还需要修改头节点的指针,不然的话就不需要更新头节点的指针。不然的话既需要修改该索引处的节点的前一个节点指向当前待插入元素,并且使索引处的前一个节点指针为当前带插入的元素。注意最后修了改链表的长度。

e:emove(int)

  

 public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
} 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;
}

  移除某一个节点的元素,首先判断输入的索引位置是否合法,然后调用unlink(Node)方法进行移除,通过unlink()方法的源码我们可以看到它首先获取节点处的元素还有的前一个以及后一个节点指针。如果该节点是链表第一个节点即头节点,那么需要把头指针修改为指向该节点的下一个指向节点next;否则只需要修改当前节点的上一个节点指向该节点的下一个节点即可;然后如果该节点是最后一个元素那么只需要修改链表的尾指针指向当前元素的上一个节点即可,否则的话修改前节点上一个节点的指针指向该节点的下一个指针指向,然后是该节点的下一个指向节点设置为null,注意最后更新链表的长度。

f:peek()

  

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

  获取第一个链表节点的值,判断是否为空,不为空则返回节点对应的值。

g:element()

 public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}

  返回第一个节点的值。

h:poll()

  public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
} private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}

  删除链表的首节点;如果节点不为空就调用unlinkFirst(Node)方法,做的事情很简单,删除首节点,垃圾回收还有就是更新链表长度。

  i:remove()

  public E remove() {
return removeFirst();
} public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

  和poll()方法有很大的相似地方

j:offer(E)

  public boolean offer(E e) {
return add(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
} public boolean offerLast(E e) {
addLast(e);
return true;
}
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

offer(E)将元素添加到链表的末尾;offerFirst(E)将元素添加到元素首部,offerLast(E)方法作用和offer(E)是一样的。

K:push(E)

 public void push(E e) {
addFirst(e);
}
public void addFirst(E e) {
linkFirst(e);
} private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

将元素插入到头部,没什么好说的。

l:pop()

   public E pop() {
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) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}

将第一个元素删除,头部节点,前面也讲解过;

4.其他(小结)

  ArrayList是基于线性表的顺序存储表,LinkedList是基本线性表的链表存储表。对于新增和删除元素,LinkedList比较占有优势,只需要变前后2个节点,而ArrayList要移动数据。对于随机访问来说,ArrayList比较占有优势,可以根据索引号快速访问,而LinkedList则需要遍历集合的元素来定位。而对于迭代操作(iterate)和查找操作(indexOf),两者是差不多。我们可以认为需要随机访问较多的那么比较适合用ArrayList,如果是插入和删除(如消息队列)较多的那么就需要考虑LinkedList。

Java 集合之LinkedList源码分析的更多相关文章

  1. 死磕 java集合之LinkedList源码分析

    问题 (1)LinkedList只是一个List吗? (2)LinkedList还有其它什么特性吗? (3)LinkedList为啥经常拿出来跟ArrayList比较? (4)我为什么把LinkedL ...

  2. Java集合之LinkedList源码分析

    概述 LinkedLIst和ArrayLIst一样, 都实现了List接口, 但其内部的数据结构不同, LinkedList是基于链表实现的(从名字也能看出来), 随机访问效率要比ArrayList差 ...

  3. Java集合干货——LinkedList源码分析

    前言 在上篇文章中我们对ArrayList对了详细的分析,今天我们来说一说LinkedList.他们之间有什么区别呢?最大的区别就是底层数据结构的实现不一样,ArrayList是数组实现的(具体看上一 ...

  4. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  5. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  6. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  7. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  8. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  9. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

随机推荐

  1. Monkey源代码分析番外篇WindowManager如何出的喷射事件的进程间的安全限制

    在分析monkey源代码时的一些背景知识不明确,例如看到monkey它是用windowmanager的injectKeyEvent的喷射事件时的方法.我发现自己陷入疙瘩,这种方法不仅能够在当前的应用程 ...

  2. HTTP 请求报文 响应报文(转)

    引言 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议.所有的WWW文件都必须遵守这个标准.设计HTTP最初的目的是为了提供一种发 ...

  3. mySQl该数据库不能将中国的做法

    1. 更改MySQL安装文件夹(C:\Program Files\MySQL\MySQL Server 5.5)的my.ini文件 设置: default-character-set=utf8 cha ...

  4. 霸气侧漏HTML5--之--canvas(1) api + 弹球例子

    html5也许最有吸引力的新功能是canvas 漆.基本可以足够强大后,以取代flash页面的效果.下面来介绍canvas要使用: HTML5 Canvas的基本图形都是以路径为基础的.通常使用Con ...

  5. java多线程学习(两)——创建一个线程

    一个.java创建两个线程的方法 1.从java.lang.Thread派生一个新类线程类,其覆盖run()方法 2.实现Runnable接口.重载Runnable接口中的run()方法. 使用Thr ...

  6. 百度官方wormHole后门检测记录(转)

    本次这个安卓后门真是哔了狗了,刚加入大安卓阵营就出了这个篓子,跟你root不root和什么安卓版本没有版毛钱关系,完全是百度SDK官方提供的后门,不是漏洞,人为的. 乌云地址:http://drops ...

  7. Writing your first Django app, part 1(转)

    Let’s learn by example. Throughout this tutorial, we’ll walk you through the creation of a basic pol ...

  8. hdu 4975 最大流问题解决队伍和矩阵,利用矩阵dp优化

    //刚開始乱搞. //网络流求解,假设最大流=全部元素的和则有解:利用残留网络推断是否唯一, //方法有两种,第一种是深搜看看是否存在正边权的环.见上一篇4888 //至少四个点构成的环,另外一种是用 ...

  9. 【C++】智能指针auto_ptr简单的实现

    //[C++]智能指针auto_ptr简单的实现 #include <iostream> using namespace std; template <class _Ty> c ...

  10. 你可以不知道原因,但是,我们不能停止努力。httplook抓取路由配置界面信息

    因为实验室的项目要求,需要动态控制路由的配置信息,我们知道.路由选择需要购买的访问后,:http://192.168.1.1 路由配置页面.配置,不须要人手工操作.甚至定时任务配置的时候,就须要进一步 ...