前言

上篇中,我们分析了ArrayList的常用方法及其实现机制。ArrayList是基于内存空间连续的数组来实现的,List中其实还提供了一种基于链表结构的LinkedList来实现集合。同时多线程的操作,还提供了线程安全的Vector实现,以及栈实现的Stack。

3.LinkedList

看下LinkedList的继承、实现关系:

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

可以看到它与ArrayList是有区别的,继承的不再是AbstractList而是AbstractSequentialList,同时它还实现了Deque接口。

Deque是“double ended queue“的缩写,意为双端队列,它定义了一些FIFO(先进先出)的队列实现方法以及LIFO(后进先出)栈的实现方法。而LinkedList实现了该接口,所以自然而然LinkedList也可以作为队列和栈的实现来使用。

3.1 LinkedList的构造函数

 /**
* 空的实现
*/
public LinkedList() {
} /**
* 以指定的集合构造LinkedList
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

LinkedList与ArrayList不同,它不涉及初始化集合大小的操作,所以一般使用都是直接空的实现就可以了。因为没有大小限制,所以LinkedList没有扩容一说,当新添加一个元素直接改变尾结点的指向且将当前结点指定为尾结点即可。

3.2 LinkedList的成员变量

   transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first; /**
* Pointer to last node.
* 恒等式 当List为空时 first和last为null,当List不为空时,last结点的next指向null且last结点item不为null
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last; /**
* 结点内部类
*
*/
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;
}
}

可以看到,LinkedList定义了三个成员变量,当前链表的size大小,以及双向链表的头结点和尾结点。还有一个是AbstractList的modCount用来计数集合的变化次数的变量。其内部还定义了私有的静态内部类Node,它主要的作用就是描述链表结点。包含一个前驱结点引用,后继结点引用以及自身的item值,这个学过数据结构的都应该清楚,不再赘述。

为什么ArrayList实现的是AbstractList而LinkedList则是要实现AbstractList的子类AbstractSequentialList呢?

我们可以先看下AbstractSequentialList实现:

protected AbstractSequentialList() {
}
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public E set(int index, E element) {
try {
ListIterator<E> e = listIterator(index);
E oldVal = e.next();
e.set(element);
return oldVal;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public void add(int index, E element) {
try {
listIterator(index).add(element);
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
} public E remove(int index) {
try {
ListIterator<E> e = listIterator(index);
E outCast = e.next();
e.remove();
return outCast;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
} public boolean addAll(int index, Collection<? extends E> c) {
try {
boolean modified = false;
ListIterator<E> e1 = listIterator(index);
Iterator<? extends E> e2 = c.iterator();
while (e2.hasNext()) {
e1.add(e2.next());
modified = true;
}
return modified;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public Iterator<E> iterator() {
return listIterator();
} public abstract ListIterator<E> listIterator(int index);

如果对上篇中的AbstractList的源码有印象的话,不难发现AbstractSequentialList主要是继续重写了AbstractList的中部分方法。譬如:get(int index)、set(int index,E element)、remove(int index)等方法,它其实是对于基于顺序访问结构的集合再次提供一个骨干实现。如果你需要自己实现一个基于顺序遍历元素的链表实现,可以只需要实现listIterator()方法就可以了,但实际上LinkedList中对于上述方法也是覆写了自己的实现。接下来分析下LinkedList中具体实现。

3.3 LinkedList元素的访问

 get(int index)

  /**
* 获取指定索引的元素
*/
public E get(int index) {
//校验索引是否非法
checkElementIndex(index);
return node(index).item;
} /**
* Node结点 内部类方法
*/
Node<E> node(int index) {
//index与集合一半进行比较,索引位于前半部分则用first遍历到目标元素
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;
}
}

通过源码发现,当通过get(index)方法来获取某个序列值时,先将index与size的一半进行比较,位于前半部分则用first结点,后半部分则用last结点。然后通过迭代遍历next或者previous结点来寻找目标结点,也正是基于此,所以链表结构的访问目标元素过程耗时要比数组访问的费时。

add(E e)

  /**
* 添加一个指定的元素到链表的尾部
*/
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
/** 新创建的结点的next为null
* 前驱结点引用指向原last,所以印证该链表为双向链表而非双向循环链表
*/
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

看到它的添加元素方法,就是直接心生成一个结点并且将该结点的前驱结点引用指向原last结点,next为null。故而由此可以印证我们之前的论述:LinkedList内部是基于双向链表来实现的但非双向循环链表

remove(Object o)

   /**
* 将元素移除出链表
*/
public boolean remove(Object o) {
//object为null,则直接从first便利第一个为null的元素予以剔除
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
//不为null则直接根据equals找出第一个相等的元素予以剔除
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

删除元素时,首先从first结点开始,根据equals依次遍历到第一个与指定Object对象相等的元素进入删除步骤。看下如何删除后做了什么操作:

 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;
//prev为null,说明当前结点为头结点
if (prev == null) {
first = next;
} else {
//改变前驱结点的next指向,同时目标结点前驱置空
prev.next = next;
x.prev = null;
}
//next,说明当前结点为尾结点
if (next == null) {
last = prev;
} else {
//改变后继结点的prev指向,同时目标结点后继置空
next.prev = prev;
x.next = null;
}
//改变size、modCount计数
x.item = null;
size--;
modCount++;
return element;
}

在链表结构中,删除某一个元素结点后,需要改变前驱结点的后继指向,后继结点的前驱指向,同时自身元素的item、next、prev都置为null。remove(int index)方法与上述方法类似,无非就是先根据索引找到执行元素进行操作,在此不再赘述。

3.4 LinkedList序列化

与ArrayList一样,LinkedList中的三个成员变量均被transient修饰,所以需要自身实现readObject()方法和writeObject()方法。

/**
* 覆写了readObject方法
* 首先读取size大小,然后调用linkLast重新添加元素结点 构造双向链表
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject(); // Read in size
int size = s.readInt(); // Read in all elements in the proper order.
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
} /**
* 覆写了writeObject方法
* 首先写入size大小,然后是item的值
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject(); // Write out size
s.writeInt(size); // Write out all elements in the proper order.
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}

可以看到,LinkedList的全部成员变量都是声明为transient类型的,所以进行序列化操作时,都将忽略,在自定义实现中,首先是将size写入,然后把结点中的item值部分按照顺序依次吸入,在反序列中,则是依此调用添加方法,重新生成Node结点类型的数据,达到反序列的目的。

3.5 LinkedList作为队列和栈实现

由于LinikedList实现了List接口和Deque接口,因此LinkedList既可以当做普通的List集合使用,也可以当作用FIFO(先进先出)队列,也可以当作LIFO(后进先出)堆栈。

LIFO(后进先出)栈

E peek();

返回栈顶元素,栈为空时返回null

void push(E e);

往栈内添加元素

E pop();

移除栈顶元素,并返回此元素(出栈)

FIFO(先进先出)队列

boolean offer(E e);// 等效boolean add(E e)

往队列中(队尾)添加元素

E poll();//E remove()

移除队列(队首)元素(出队列),返回此元素

E getFirst();

获取队首元素

E getLast();

获取队尾元素

以上方法只有当你声明为Deque的实现类时才可使用,接下来看个示例代码:

package com.ant3;

import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; /**
* @author ant.world
* @date 2016年5月24日
* @version 1.0
* @Description
*/
public class Son { public static void main(String[] args) {
/**
* LinkedList堆栈使用
*/
Deque<String> stack = new LinkedList<String>();
stack.push("aa");
stack.push("bb");
stack.push("cc");
Iterator<String> it = stack.iterator();
System.out.println("堆栈数据");
while(it.hasNext()){
System.out.print(it.next()+"\t");
}
System.out.println();
System.out.println("peek查看栈顶元素"+stack.peek());
System.out.println("pop移除元素"+stack.pop());
System.out.println("堆栈数据2");
it = stack.iterator();
while(it.hasNext()){
System.out.print(it.next()+"\t");
} } }
}

以上为栈的实例代码,从栈顶存入元素,出栈操作和入栈操作以及返回栈顶元素。看下队列的相关示例代码:

package com.ant4;

import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList; /**
* @author ant.world
* @date 2016年6月15日
* @version 1.0
* @Description
*/
public class MyDeque{ public static void main(String[] args) {
Deque<String> queue = new LinkedList<String>();
queue.offer("aa");//等效 queue.add("aa");
queue.offer("bb");
queue.offer("cc"); Iterator<String> it = queue.iterator();
while(it.hasNext()){
System.out.print("队列中元素:"+it.next()+"\t");
}
System.out.println();
System.out.println("出队列:"+queue.poll());
it = queue.iterator();
while(it.hasNext()){
System.out.print("队列中元素:"+it.next()+"\t");
}
/**
* 打印结果
*
队列中元素:aa 队列中元素:bb 队列中元素:cc
出队列:aa
队列中元素:bb 队列中元素:cc
*/
} }

总结

看完了ArrayList和LinkedList,简单总结下它们的之间的异同,及其使用场景:

1.实现方式的不同:ArrayList是基于数组来实现,LinkedList是基于双向(非循环)链表来实现的。

2.寻址空间:ArrayList是连续的存储空间,范围不够时,扩容为原来的1.5倍,LinkedList可在非连续的物理存储空间,通过指针连接起来。

3.访问、修改元素;因为两者存储方式的不同,ArrayList访问元素时只需要根据数组首地址的偏移量就能够找到元素。而LinkedList因为内存空间的不连续,需要通过遍历指针来访问某个元素。所以访问查找元素时,ArrayList明显要优于LinkedList。

4.指定位置新增、删除元素;新增、删除元素时,ArrayList需要保证其存储空间的连续性,需要移动元素。LinkedList则只需要选定元素后,改变其指针的指向,达到新增、删除元素的目的。所以新增、删除元素时,LinkedList要优于ArrayList。

如果是顺序添加时ArrayList则是直接添加到列表size+1位置上的,此时不涉及到移动元素。

其实到这里,我们可以有个小疑问,既然集合类元素的可以有基于数组和链表这两种不同的数据结构来来实现,那么栈和队列有没有基于数组来实现的呢?答案肯定是有,那就是Stack。该部分下节再述。

Java-集合类源码List篇(二)的更多相关文章

  1. Java集合类源码解析:Vector

    [学习笔记]转载 Java集合类源码解析:Vector   引言 之前的文章我们学习了一个集合类 ArrayList,今天讲它的一个兄弟 Vector.为什么说是它兄弟呢?因为从容器的构造来说,Vec ...

  2. Java集合类源码解析:HashMap (基于JDK1.8)

    目录 前言 HashMap的数据结构 深入源码 两个参数 成员变量 四个构造方法 插入数据的方法:put() 哈希函数:hash() 动态扩容:resize() 节点树化.红黑树的拆分 节点树化 红黑 ...

  3. Java集合源码分析(二)ArrayList

    ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...

  4. java集合类源码学习二

    我们查看Collection接口的hierarchy时候,可以看到AbstractCollection<E>这样一个抽象类,它实现了Collection接口的部分方法,Collection ...

  5. Java集合源码分析(二)Linkedlist

    前言 前面一篇我们分析了ArrayList的源码,这一篇分享的是LinkedList.我们都知道它的底层是由链表实现的,所以我们要明白什么是链表? 一.LinkedList简介 1.1.LinkedL ...

  6. Java集合类源码解析:ArrayList

    目录 前言 源码解析 基本成员变量 添加元素 查询元素 修改元素 删除元素 为什么用 "transient" 修饰数组变量 总结 前言 今天学习一个Java集合类使用最多的类 Ar ...

  7. Java集合类源码解析:AbstractList

    今天学习Java集合类中的一个抽象类,AbstractList. 初识AbstractList AbstractList 是一个抽象类,实现了List<E>接口,是隶属于Java集合框架中 ...

  8. Java集合类源码分析

    常用类及源码分析 集合类 原理分析 Collection   List   Vector 扩充容量的方法 ensureCapacityHelper很多方法都加入了synchronized同步语句,来保 ...

  9. Java集合类源码解析:AbstractMap

    目录 引言 源码解析 抽象函数entrySet() 两个集合视图 操作方法 两个子类 参考: 引言 今天学习一个Java集合的一个抽象类 AbstractMap ,AbstractMap 是Map接口 ...

  10. java Thread源码分析(二)

    一.sleep的使用 public class ThreadTest { public static void main(String[] args) throws InterruptedExcept ...

随机推荐

  1. 关于git 提交到分支

    想必大家对于github并不陌生,但是有时候我们提交到github上的页面,想将静态的页面展示给别人看,所以这个时候,需要创建一个gh-pages的分支,然后利用 https://you github ...

  2. JavaScript通过preventDefault()使input[type=text]禁止输入但保留光标

    一.说明 取消事件的默认动作. 该方法将通知 Web 浏览器不要执行与事件关联的默认动作(如果存在这样的动作).例如,如果 type 属性是 "submit",在事件传播的任意阶段 ...

  3. testlink1.9.3测试管理工具安装

    一.下载testlink1.9.3rar安装包 下载地址:http://download.csdn.net/download/u010082992/7678491 二.安装IIS 在Server 20 ...

  4. 一起talk C栗子吧(第七十八回:C语言实例--创建进程)

    各位看官们,大家好.上一回中咱们说的是DIY ls命令续的样例.这一回咱们说的样例是:创建进程.闲话休提.言归正转. 让我们一起talk C栗子吧! 看官们.关于进程的概念,我们简单做个简单的介绍:进 ...

  5. [二次开发]dede文章页面怎样显示作者的头像

    dede在文章页面显示作者仅仅是显示其username,可是假如我想把dede改造成较为社交化的站点.我认为是有必要显示作者的头像的,可是官方并没有相应的模版标签. 在网上看到解决问题的办法基本上是直 ...

  6. 5 Best VPNs for Ubuntu

    https://www.bestvpn.com/blog/6268/5-best-vpns-for-ubuntu/?nabe=6412130213429248:0&utm_referrer=h ...

  7. Win10在右键菜单添加“在此处打开命令窗口”设置项

    Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\Directory\shell\OpenCmdHere] @="在此处打开命令 ...

  8. 解决svnserve: Can't bind server socket: Address already in use

    最近在忙着搭建jenkins系统集成版本控制和git分布式版本控制,其中涉及到了点svn方面的,由于自己也是第一次搭建svn,挺顺利的,中间遇到点小问题: 我使用的是yum安装的svn,安装完成配置结 ...

  9. 剑指offer 面试61题

    面试61题: 题目:LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到 ...

  10. gearman管理

    通常,Gearman被用来分发任务,以便实现异步操作.下面捋捋如何管理Gearman. 说明:请自行安装好Gearman和PHP PECL Gearman. (我之前安装的gearman php的c语 ...