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. STL源代码分析 集装箱 stl_set.h

    本文senlie原版的,转载请保留此地址:http://blog.csdn.net/zhengsenlie set ------------------------------------------ ...

  2. APP漏洞导致移动支付隐患重重,未来之路怎样走?

    没有一种支付是100%安全的,互联网及移动支付规模的增长,其交易的安全性须要银行.支付公司.App开发人员.用户等參与各方更加重视.当下手机支付似乎变成了一种时尚,用户们"刷手机" ...

  3. NSIS:获取硬盘中容量最大的分区盘符

    原文 NSIS:获取硬盘中容量最大的分区盘符 我们在安装一些在线视频软件比如迅雷看看时,会发现他们的安装程序会自动判断当前系统中容量最大的分区,以便在其中创建数据缓冲下载的文件夹,这种功能如果实现呢, ...

  4. 整理php操作memcache缓存为基础的方法

    php操作memcache共享缓存方法 采用memcache的前提下,是需要在服务器端被配置memcahche环境! 证实memcahce经过正常的连接可以在程序中使用! <?php /** * ...

  5. Linux内核分析(二)----内核模块简介|简单内核模块实现

    原文:Linux内核分析(二)----内核模块简介|简单内核模块实现 Linux内核分析(二) 昨天我们开始了内核的分析,网上有很多人是用用源码直接分析,这样造成的问题是,大家觉得很枯燥很难理解,从某 ...

  6. java生成二维码(带logo)

    之前写过一篇不带logo的二维码实现方式,採用QRCode和ZXing两种方式 http://blog.csdn.net/xiaokui_wingfly/article/details/3947618 ...

  7. PL/SQL编程(1) - 存储过程,函数以及参数

    存储过程 PROCEDURE [schema.]name[( parameter[, parameter...] ) ] [AUTHID DEFINER | CURRENT_USER ] [ACCES ...

  8. Git & Github 一页简明笔记(转)main

    由于小组工程需要使用git&github的版本控制来协作,但我对其使用并不熟悉,特此写篇一页的笔记放在手边,备随时查阅. 使用方法:常用命令供随时查阅,其余内容供新手了解. 0. 常用命令一览 ...

  9. EasyUI 扩展自己定义EasyUI校验规则 验证规则(经常使用的)

    比如 校验输入框仅仅能录入0-1000之间 最多有2位小数的数字 表单<input type="text" id="rate" name="ra ...

  10. HTML5 Web SQL Database 与 Indexed Database 的 CRUD 操作

    http://www.ibm.com/developerworks/cn/web/1210_jiangjj_html5db/ 版权声明:本文博客原创文章,博客,未经同意,不得转载.