【Java集合】ArrayDeque源码解读
简介
双端队列是一种特殊的队列,它的两端都可以进出元素,故而得名双端队列。
ArrayDeque
是一种以循环数组方式实现的双端队列,它是非线程安全的。
它既可以作为队列也可以作为栈。
继承体系
ArrayDeque
实现了 Deque
接口,Deque
接口继承自 Queue
接口,它是对 Queue
的一种增强。
同时实现了 Serializable
和 Cloneable
接口,可以进行序列化和克隆。
源码解读
主要属性
// 存储元素的数组
transient Object[] elements; // non-private to simplify nested class access
// 队列头位置
transient int head;
// 队列尾位置
transient int tail;
// 最小初始容量
private static final int MIN_INITIAL_CAPACITY = 8;
// 序列号
private static final long serialVersionUID = 2340985798034038923L;
head
指向头元素
tail
指向尾元素的下一个位置
这里注意到,head
,tail
,elements
属性都被 transient
修饰,不会参与序列化。
可能会有疑问,**elements**
要是不参与序列化,集合内的数据不就无法持久化吗。
这个问题先放在这里,讲完 ArrayList
扩容原理之后再进行回答。
构造方法
// 默认构造方法,初始容量为16
public ArrayDeque() {
elements = new Object[16];
}
// 指定元素个数初始化
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
// 将集合c中的元素初始化到数组中
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
// 初始化数组
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
// 计算容量,这段代码的逻辑是算出大于numElements的最接近的2的n次方且不小于8
// 比如,3算出来是8,9算出来是16,33算出来是64
private static int calculateSize(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) {
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
return initialCapacity;
}
通过构造方法,我们知道默认初始容量是16,最小容量是8。
这里比较有意思的是 calculateSize
容量计算方法,本质是为了获取大于当前数值的最小的2的幂,比如 3 算出来是 8,9 算出来是 16,33 算出来是 64。
由于 2 的幂用二进制表示的特点就是只有一个二进位位是 1 ,其余数位都是 0,所以从二进制的角度,分为两步操作
- 第一步:将该数二进制的最高位 1 之后的所有数位设置为 1(如果 numElements < 8 则直接返回 8)
// 第一步
0000 0001 0101 1110 1000 1111 0001 1010 // 原数
0000 0001 1111 1111 1111 1111 1111 1111 // 第一步完成
- 第二步:原数加一(如果小于 0,说明超过最大容量,整体右移一位)
// 第二步
0000 0001 1111 1111 1111 1111 1111 1111 // 第一步完成
0000 0010 0000 0000 0000 0000 0000 0000 // 第二部完成,成为 2 的幂
对于calculateSize
一种直接的想法是使用循环加位运算,找到最高位的二进制 1(形成独立的一个 2 的幂),然后将该数位左移一位返回,时间复杂度 O(n)
,最坏情况下需要进行 31 次。
int tmp = 1 << 31;
int count = 31;
while ((numElements & tmp) == 0 && count > 0) {
tmp >>>= 1;
count--;
}
tmp <<= 1;
return tmp;
源码利用的是二分的思想,总共 32 位也就是 2 的 5 次方,只需要 5 次位运算即可,时间复杂度 O(logn)
0000 0001 0000 0000 0000 0000 0000 0000
0000 0000 1000 0000 0000 0000 0000 0000 >>> 1
0000 0001 1000 0000 0000 0000 0000 0000 |=
0000 0000 0110 0000 0000 0000 0000 0000 >>> 2
0000 0001 1110 0000 0000 0000 0000 0000 |=
0000 0000 0001 1110 0000 0000 0000 0000 >>> 4
0000 0001 1111 1110 0000 0000 0000 0000 |=
0000 0000 0000 0001 1111 1110 0000 0000 >>> 8
0000 0001 1111 1111 1111 1111 0000 0000 |=
0000 0000 0000 0000 0000 0001 1111 1111 >>> 16
0000 0001 1111 1111 1111 1111 1111 1111 |=
扩容
private void doubleCapacity() {
// 断言集合已满
assert head == tail;
// 头指针的位置
int p = head;
// 旧数组长度
int n = elements.length;
// 头指针离数组尾的距离
int r = n - p; // number of elements to the right of p
// 新长度为旧长度的两倍
int newCapacity = n << 1;
// 判断是否溢出
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
// 新建新数组
Object[] a = new Object[newCapacity];
// 将旧数组head之后的元素拷贝到新数组中
System.arraycopy(elements, p, a, 0, r);
// 将旧数组下标0到head之间的元素拷贝到新数组中
System.arraycopy(elements, 0, a, r, p);
// 赋值为新数组
elements = a;
// head指向0,tail指向旧数组长度表示的位置
head = 0;
tail = n;
}
扩容原理:集合满了之后,创建一个原数组容量 2 倍的集数组,然后把元素拷贝到新数组中。
数组拷贝使用的是 System.arraycopy
函数
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
// src – the source array.
// srcPos – starting position in the source array.
// dest – the destination array.
// destPos – starting position in the destination data.
// length – the number of array elements to be copied.
ok,讲完扩容之后补一下坑,elements
不参与序列化是从空间的角度考虑的,ArrayDeque
的容量始终为 2 的幂,始终不是满的,有位置没有存放元素,如果是刚刚扩容完,可能有接近一半的空间未使用,如果参与序列化,会造成大量空间的浪费,消耗网络传输或者数据库传输,降低吞吐量。
解决方案是把集合拆分成几部分进行传输,而不是作为一个整体,来节约空间和减少序列化的时间
// 将 ArrayDeque 实例的状态保存到流(即序列化它)
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 写出当前类的所有非静态字段(non-static)和非瞬态字段(non-transient)到ObjectOutputStream
s.defaultWriteObject();
// Write out size
// 将size写出到ObjectOutputStream
s.writeInt(size());
// Write out elements in order.
int mask = elements.length - 1;
// i = (i + 1) & mask 表示循环数组下标的移动
for (int i = head; i != tail; i = (i + 1) & mask)
s.writeObject(elements[i]); // 有序的将elementData中已使用的元素读出到流中
}
// 从流中重构 ArrayDeque 实例(即,对其进行反序列化)
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 读入size和非transient非static属性
s.defaultReadObject();
// Read in size and allocate array
// 读入容量
int size = s.readInt();
// 重新分配容量
int capacity = calculateSize(size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
allocateElements(size);
head = 0;
tail = size;
// Read in all elements in the proper order.
// // 按正确的顺序读入所有元素。
for (int i = 0; i < size; i++)
elements[i] = s.readObject();
}
入队
// 从队列头入队
public void addFirst(E e) {
// 不允许null元素
if (e == null)
throw new NullPointerException();
// 将head指针减1并与数组长度减1取模
// 这是为了防止数组到头了边界溢出
// 如果到头了就从尾再向前
// 相当于循环利用数组
elements[head = (head - 1) & (elements.length - 1)] = e;
// 如果头尾挨在一起了,就扩容
// 扩容规则也很简单,直接两倍
if (head == tail)
doubleCapacity();
}
// 从队列尾入队
public void addLast(E e) {
// 不允许null元素
if (e == null)
throw new NullPointerException();
// 在尾指针的位置放入元素
// 可以看到tail指针指向的是队列最后一个元素的下一个位置
elements[tail] = e;
// tail指针加1,如果到数组尾了就从头开始
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
- 入队有两种方式,从队列头或者从队列尾;
- 如果容量不够了,直接扩大为两倍;
- 通过取模的方式让头尾指针在数组范围内循环;
x & (len - 1) = x % len
,使用&
的方式更快;
public boolean add(E e) {
addLast(e);
return true;
}
public boolean offer(E e) {
return offerLast(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
- 剩下几种入队操作本质都是
addFirst
和addLast
,不过是多了返回值。
出队
// 从队列头出队
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
// 取队列头元素
E result = (E) elements[h];
// 如果队列为空,就返回null
if (result == null)
return null;
// 将队列头置为空
elements[h] = null; // Must null out slot
// 队列头指针右移一位
head = (h + 1) & (elements.length - 1);
// 返回取得的元素
return result;
}
// 从队列尾出队
public E pollLast() {
// 尾指针左移一位
int t = (tail - 1) & (elements.length - 1);
@SuppressWarnings("unchecked")
// 取当前尾指针处元素
E result = (E) elements[t];
// 如果队列为空返回null
if (result == null)
return null;
// 将当前尾指针处置为空
elements[t] = null;
// tail指向新的尾指针处
tail = t;
// 返回取得的元素
return result;
}
- 出队有两种方式,从队列头或者从队列尾;
- 通过取模的方式让头尾指针在数组范围内循环;
- 出队之后没有缩容
// 移除队头元素
public E removeFirst() {
E x = pollFirst();
if (x == null)
throw new NoSuchElementException();
return x;
}
// 移除队尾元素
public E removeLast() {
E x = pollLast();
if (x == null)
throw new NoSuchElementException();
return x;
}
// 移除队头元素
public E remove() {
return removeFirst();
}
// 移除队头元素
public E poll() {
return pollFirst();
}
剩下几种出队操作本质是 pollFirst
和 pollLast
,区别就是 remove*
操作可能抛出 NoSuchElementException
异常。
入栈
public void push(E e) {
addFirst(e);
}
出栈
public E pop() {
return removeFirst();
}
入栈和出栈操作本质都是操作队列头。
容量
public int size() {
return (tail - head) & (elements.length - 1);
}
用与运算取代取模运算,速度更快。
查看两端元素
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}
@SuppressWarnings("unchecked")
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}
如果元素不存在,返回 null
public E getFirst() {
@SuppressWarnings("unchecked")
E result = (E) elements[head];
if (result == null)
throw new NoSuchElementException();
return result;
}
/**
* @throws NoSuchElementException {@inheritDoc}
*/
public E getLast() {
@SuppressWarnings("unchecked")
E result = (E) elements[(tail - 1) & (elements.length - 1)];
if (result == null)
throw new NoSuchElementException();
return result;
}
如果元素不存在,抛出 NoSuchElementException
异常
是否为空
public boolean isEmpty() {
return head == tail;
}
head
和 tail
相同时表示为空
清空
public void clear() {
int h = head;
int t = tail;
// 如果 head == tail 则为空,直接返回,指向哪里无所谓,是循环数组
if (h != t) { // clear all cells
// 如果 head != tail 表示有元素,head 和 tail 都指向 0
head = tail = 0;
int i = h;
int mask = elements.length - 1;
// 从头元素开始循环清空数组
do {
elements[i] = null;
i = (i + 1) & mask;
} while (i != t);
}
}
性能测试
ArrayDeque 与 LinkedList
ArrayDeque
跟同样实现了 Deque
接口的 LinkedList
对比。
- 二者都添加 200000 个数据。
long start = 0, end = 0;
start = System.currentTimeMillis();
LinkedList linkedList = new LinkedList();
for (int i=0; i<2000000; i++) {
linkedList.addFirst(i);
}
end = System.currentTimeMillis();
System.out.println("LinkedList addFirst 2000000 cost time = " + (end-start) + "ms");
LinkedList addFirst 2000000 cost time = 351ms
long start = 0, end = 0;
ArrayDeque arrayDeque = new ArrayDeque();
start = System.currentTimeMillis();
for (int i=0; i < 2000000; i++){
arrayDeque.addFirst(i);
}
end = System.currentTimeMillis();
System.out.println("ArrayDeque addFirst 2000000 cost time = " + (end-start) + "ms");
ArrayDeque addFirst 2000000 cost time = 20ms
可以看到,ArrayDeque
是 LinkedList
速度的 15 倍
- 二者都移除 200000 个数据。
start = System.currentTimeMillis();
while (linkedList.size() != 0) {
linkedList.removeFirst();
}
end = System.currentTimeMillis();
System.out.println("LinkedList removeFirst cost time = " + (end-start) + "ms");
LinkedList removeFirst cost time = 21ms
start = System.currentTimeMillis();
while (arrayDeque.size() != 0) {
arrayDeque.removeFirst();
}
end = System.currentTimeMillis();
System.out.println("ArrayDeque removeFirst cost time = " + (end-start) + "ms");
ArrayDeque removeFirst cost time = 10ms
可以看到,ArrayDeque
是 LinkedList
速度的 2 倍
【Java集合】ArrayDeque源码解读的更多相关文章
- Java集合ArrayList源码解读
最近在回顾数据结构,想到JDK这样好的代码资源不利用有点可惜,这是第一篇,花了心思.篇幅有点长,希望想看的朋友认真看下去,提出宝贵的意见. :) 内部原理 ArrayList 的3个字段 priva ...
- 【java集合框架源码剖析系列】java源码剖析之TreeSet
本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...
- 【java集合框架源码剖析系列】java源码剖析之HashSet
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...
- 【java集合框架源码剖析系列】java源码剖析之TreeMap
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于TreeMap的知识. 一TreeMap的定义: public class TreeMap&l ...
- 【java集合框架源码剖析系列】java源码剖析之ArrayList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...
- 【java集合框架源码剖析系列】java源码剖析之LinkedList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 在实际项目中LinkedList也是使用频率非常高的一种集合,本博客将从源码角度带领大家学习关于LinkedList的知识. ...
- 【java集合框架源码剖析系列】java源码剖析之HashMap
前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...
- Java集合框架源码(二)——hashSet
注:本人的源码基于JDK1.8.0,JDK的版本可以在命令行模式下通过java -version命令查看. 在前面的博文(Java集合框架源码(一)——hashMap)中我们详细讲了HashMap的原 ...
- java集合类型源码解析之ArrayList
前言 作为一个老码农,不仅要谈架构.谈并发,也不能忘记最基础的语言和数据结构,因此特开辟这个系列的文章,争取每个月写1~2篇关于java基础知识的文章,以温故而知新. 如无特别之处,这个系列文章所使用 ...
随机推荐
- shiro之第一个程序认证
有关shiro的介绍请访问https://blog.csdn.net/Kevinnsm/article/details/111823268 三个核心组件:Subject, SecurityManage ...
- 12-factors
12-factors 官方网址 The Twelve-Factor App 简介 如今,软件通常会作为一种服务来交付,它们被称为网络应用程序,或软件即服务(SaaS).12-Factor 为构建如下的 ...
- python中faker模块的使用
Faker 安装 pip install Faker 基本使用 from faker import Faker #创建对象,默认生成的数据为为英文,使用zh_CN指定为中文 fake = Faker( ...
- metasploit基本命令
一.核心命令 ? 帮助命令 banner 显示一个真棒metasploite横幅 cd 更改当前的工作目 color 切换颜色 connect 连接与主机通信 exit 退出控制台 get 获取特定于 ...
- Redis4.0.14 迁槽失败
线上一个redis集群中主节点使用的内存达到了9.78g,按照redis单个实例最大内存不要超出10g的规范,扩容操作就放在了今天晚上进行.因为之前redis迁槽都是采用 redis-trib.rb ...
- 你不知道的下划线属性-text-decoration
大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...
- WebSocket 协议详解
一.WebSocket 协议背景 早期,在网站上推送消息给用户,只能通过轮询的方式或 Comet 技术.轮询就是浏览器每隔几秒钟向服务端发送 HTTP 请求,然后服务端返回消息给客户端. 轮询技术一般 ...
- SpringBoot中异常处理
一.背景 在我们编写程序的过程中,程序中可能随时发生各种异常,那么我们如何优雅的处理各种异常呢? 二.需求 1.拦截系统中部分异常,返回自定义的响应. 比如: 系统发生HttpRequestMetho ...
- python中的sort用法
内置的列表类型提供sort的方法 可以根据多项指标给list实例中的元素排序.在默认情况下,sort方法总是按照自然升序排列列表内的元素 #升序排列 list1=[2,3,1,2,5] list1.s ...
- 新鲜出炉:appium2.0+ 单点触控和多点触控新的解决方案
在 appium2.0 之前,在移动端设备上的触屏操作,单手指触屏和多手指触屏分别是由 TouchAction 类,Multiaction 类实现的. 在 appium2.0 之后,这 2 个方法将会 ...