Java-集合类源码List篇(二)
前言
上篇中,我们分析了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篇(二)的更多相关文章
- Java集合类源码解析:Vector
[学习笔记]转载 Java集合类源码解析:Vector 引言 之前的文章我们学习了一个集合类 ArrayList,今天讲它的一个兄弟 Vector.为什么说是它兄弟呢?因为从容器的构造来说,Vec ...
- Java集合类源码解析:HashMap (基于JDK1.8)
目录 前言 HashMap的数据结构 深入源码 两个参数 成员变量 四个构造方法 插入数据的方法:put() 哈希函数:hash() 动态扩容:resize() 节点树化.红黑树的拆分 节点树化 红黑 ...
- Java集合源码分析(二)ArrayList
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- java集合类源码学习二
我们查看Collection接口的hierarchy时候,可以看到AbstractCollection<E>这样一个抽象类,它实现了Collection接口的部分方法,Collection ...
- Java集合源码分析(二)Linkedlist
前言 前面一篇我们分析了ArrayList的源码,这一篇分享的是LinkedList.我们都知道它的底层是由链表实现的,所以我们要明白什么是链表? 一.LinkedList简介 1.1.LinkedL ...
- Java集合类源码解析:ArrayList
目录 前言 源码解析 基本成员变量 添加元素 查询元素 修改元素 删除元素 为什么用 "transient" 修饰数组变量 总结 前言 今天学习一个Java集合类使用最多的类 Ar ...
- Java集合类源码解析:AbstractList
今天学习Java集合类中的一个抽象类,AbstractList. 初识AbstractList AbstractList 是一个抽象类,实现了List<E>接口,是隶属于Java集合框架中 ...
- Java集合类源码分析
常用类及源码分析 集合类 原理分析 Collection List Vector 扩充容量的方法 ensureCapacityHelper很多方法都加入了synchronized同步语句,来保 ...
- Java集合类源码解析:AbstractMap
目录 引言 源码解析 抽象函数entrySet() 两个集合视图 操作方法 两个子类 参考: 引言 今天学习一个Java集合的一个抽象类 AbstractMap ,AbstractMap 是Map接口 ...
- java Thread源码分析(二)
一.sleep的使用 public class ThreadTest { public static void main(String[] args) throws InterruptedExcept ...
随机推荐
- grafana-----Templating
模板允许更多的互动和动态的仪表板.可以将变量用在度量查询中,不必硬编码诸如服务器.应用程序和传感器名称之类的东西.变量显示在仪表板顶部的下拉式选择框中.这些下拉菜单可以很容易地改变在你的仪表板显示的数 ...
- 两个offer如何做选择?年薪20万vs年薪15万
(附注:本文转载于:http://www.eoeandroid.com/thread-296678-1-1.html) 前些天和一个年轻的朋友谈跳槽.朋友说她需要在两个offer里面做选择.一个是年薪 ...
- vue增删改查
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Windows Server 2003 R2 With Sp2 序列号
下载地址 ed2k://|file|cn_win_srv_2003_r2_enterprise_x64_with_sp2_vl_cd1_X13-47314.iso|647686144|107F10D2 ...
- ubuntu常见错误--Could not get lock /var/lib/dpkg/lock解决(转)
通过终端安装程序sudo apt-get install xxx时出错: E: Could not get lock /var/lib/dpkg/lock - open (11: Resource t ...
- 022-Spring Boot 构建微服务实战
一.概述 二. 2.1.微服务 将原来一个大的系统,拆分成小系统 每个小系统分别开发,测试,维护 2.2.调用方 服务提供方式:rest(http)[restTemplate,httpclient]. ...
- Spring学习笔记3—声明式事务
1 理解事务 事务:在软件开发领域,全有或全无的操作被称为事务.事务允许我们将几个操作组合成一个要么全部发生要么全部不发生的工作单元. 事务的特性: 原子性:事务是由一个或多个活动所组成的一个工作单元 ...
- springboot 2.0 配置 logback
springboot2.0默认已经引入日志jar依赖,所以直接配置日志信息就可以了. 在application.properties中加入: logging.config=classpath:logb ...
- linux crontab+curl+php 实现php定时任务
首先登入Linux ->用root登入 在命令行输入 crontab -e 之后就会打开一个文件,并且是非编辑状态,则是vi的编辑界面,通过敲键盘上的i,进入编辑模式,就可以编辑内容.这个文件 ...
- zabbix-2.4.8-1添加MySQL主从状态监控
1.安装zabbix-agentyum -y install zabbix-2.4.8-1.el6.x86_64.rpm zabbix-agent-2.4.8-1.el6.x86_64.rpm 安装以 ...