前言

上篇中,我们分析了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. 巨蟒django之CRM5 学习记录&&课程记录&&班级管理&&私户的数量上限

    1.公户变私户(事务+行级锁) 2.私户的数量上限 3.班级的管理 4.课程记录管理 5.学习记录的初始化 6.展示和编辑学习记录

  2. iOS蓝牙接收外设数据自动中断

    一.错误原因 在做iOS设备作为central,与蓝牙外设连接,接收蓝牙外设传输的数据时发生蓝牙中断. 在- (void)centralManager:(CBCentralManager *)cent ...

  3. 基于网页api(接口)实现查快递

    之前在网上找到一款下载某慕课网站的java版软件,我想知道他是怎么实现:对于视频的下载的,毕竟网页源码中大都不会直接放视频的地址,但是没有公布源码,我就反编译,等到了部分“源码”,逻辑上还是有些问题, ...

  4. Python3.6全栈开发实例[024]

    24.文件a1.txt内容(注意每行中的空格是不一样的,需要对空格进行处理)序号 部门   人数   平均年龄           备注 1 python   30   26   单身狗 2 Linu ...

  5. dataTables.bootstrap 如何显示中文

    $('#table_cust').DataTable({ "oLanguage": { "sUrl": "/assets/vendors/page_z ...

  6. (转)fiddler使用简介--其一

    原文地址:https://www.cnblogs.com/miantest/p/7289694.html Fiddler基础知识 Fiddler是强大的抓包工具,它的原理是以web代理服务器的形式进行 ...

  7. (转)使用ServiceStack构建Web服务

    提到构建WebService服务,大家肯定第一个想到的是使用WCF,因为简单快捷嘛.首先要说明的是,本人对WCF不太了解,但是想快速建立一个WebService,于是看到了MSDN上的这一篇文章 Bu ...

  8. Python编码规范 -- Python Style Guide

    Python代码风格规范. @1:参数缩进:(2种形式) <1> foo = long_function_name(var1, var2, var3, var4) #第1行有参数, 第2行 ...

  9. open函数and文件处理

    一 介绍 计算机系统分为:计算机硬件,操作系统,应用程序三部分 我们用python或其他语言编写的应用程序若想要把数据永久保存下来,必须要保存于硬盘中,这就涉及到应用程序要操作硬件,应用程序是无法操作 ...

  10. PsySH——PHP交互式控制台

    PsySH PsySH is a runtime developer console, interactive debugger and REPL for PHP. PsySH是一个PHP的运行时开发 ...