为什么Java已经不推荐使用Stack了?
为什么不推荐使用Stack
Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque
为什么不推荐使用
性能低:是因为 Stack 继承自 Vector, 而 Vector 在每个方法中都加了锁。由于需要兼容老的项目,很难在原有的基础上进行优化,因此 Vector 就被淘汰掉了,使用 ArrayList 和 CopyOnWriteArrayList 来代替,如果在非线程安全的情况下可以使用 ArrayList,线程安全的情况下可以使用 CopyOnWriteArrayList 。
破坏了原有的数据结构:栈的定义是在一端进行 push 和 pop 操作,除此之外不应该包含其他 入栈和出栈 的方法,但是 Stack 继承自 Vector,使得 Stack 可以使用父类 Vector 公有的方法。
为什么现在还在用
但是为什么还有很多人在使用 Stack。总结了一下主要有两个原因。
JDK 官方是不推荐使用 Stack,之所以还有很多人在使用,是因为 JDK 并没有加 deprecation 注解,只是在文档和注释中声明不建议使用,但是很少有人会去关注其实现细节
更多的是为了笔试面试在做算法题的时候,关注点在解决问题的算法逻辑思路上,并不会关注在不同语言下 Stack 实现细节,但是对于使用 Java 语言的业务开发者,不仅需要关注算法逻辑本身,也需要关注它的实现细节
为什么推荐使用 Deque 接口替换栈
如果 JDK 不推荐使用 Stack,那应该使用什么集合类来替换栈,一起看看官方的文档。
正如图中标注部分所示,栈的相关操作应该由 Deque 接口来提供,推荐使用 Deque 这种数据结构, 以及它的子类,例如 ArrayDeque。
val stack: Deque<Int> = ArrayDeque()
使用 Deque 接口来实现栈的功能有什么好处:
- 速度比 Stack 快
这个类作为栈使用时可能比 Stack 快,作为队列使用时可能比 LinkedList 快。因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。
- 屏蔽掉无关的方法
原来的 Java 的 Stack,包含了在任何位置添加或者删除元素的方法,这些不是栈应该有的方法,所以需要屏蔽掉这些无关的方法。声明为 Deque 接口可以解决这个问题,在接口中声明栈需要用到的方法,无需管子类是如何是实现的,对于上层使用者来说,只可以调用和栈相关的方法。
Stack 和 ArrayDeque的 区别
集合类型 | 数据结构 | 是否线程安全 |
---|---|---|
Stack | 数组 | 是 |
ArrayDeque | 数组 | 否 |
Stack 常用的方法如下所示:
操作 | 方法 |
---|---|
入栈 | push(E item) |
出栈 | pop() |
查看栈顶 | peek() 为空时返回 null |
ArrayDeque 常用的方法如下所示:
操作 | 方法 |
---|---|
入栈 | push(E item) |
出栈 | poll() 栈为空时返回 nullpop() 栈为空时会抛出异常 |
查看栈顶 | peek() 为空时返回 null |
Queue介绍
Java里有一个叫做Stack的类,却没有叫做Queue的类(它是个接口名字)。当需要使用栈时,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque;既然Queue只是一个接口,当需要使用队列时也就首选ArrayDeque了(次选是LinkedList)。
Queue
Queue接口继承自Collection接口,除了最基本的Collection的方法之外,它还支持额外的insertion, extraction和inspection操作。这里有两组格式,共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。
Deque
Deque 是"double ended queue", 表示双向的队列,英文读作"deck". Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持 insert , remove 和 examine操作,由于Deque是双向的,所以可以对队列的头和尾都进行操作,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。共12个方法如下:
当把 Deque 当做FIFO的 queue 来使用时,元素是从 deque 的尾部添加,从头部进行删除的; 所以 deque 的部分方法是和 queue 是等同的。具体如下:
Deque的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了Deque与Queue相对应的接口:
下表列出了Deque与Stack对应的接口:
上面两个表共定义了Deque的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值( false 或 null )。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。虽然Deque的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看。
ArrayDeque和LinkedList是Deque的两个通用实现,由于官方更推荐使用AarryDeque用作栈和队列,加之上一篇已经讲解过LinkedList,本文将着重讲解ArrayDeque的具体实现
从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入 null 元素。
上图中我们看到, head 指向首端第一个有效元素, tail 指向尾端第一个可以插入元素的空位。因为是循环数组,所以 head 不一定总等于0, tail 也不一定总是比 head 大。
方法剖析
addFirst()
addFirst(E e)的作用是在Deque的首端插入元素,也就是在head的前面插入元素,在空间足够且下标没有越界的情况下,只需要将elements[--head] = e即可。
实际需要考虑:
- 空间是否够用
- 下标是否越界的问题
上图中,如果head为0之后接着调用addFirst(),虽然空余空间还够用,但head为-1,下标越界了。
//addFirst(E e)
public void addFirst(E e) {
if (e == null)//不允许放入null
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否越界
if (head == tail)//1.空间是否够用
doubleCapacity();//扩容
}
上述代码可以看到, 空间问题是在插入之后解决的;首先,因为tail总是指向下一个可插入的空位,也就意味着elements数组至少有一个空位,所以插入元素的时候不用考虑空间问题。
下标越界的处理解决起来非常简单,head = (head - 1) & (elements.length - 1)就可以了,这段代码相当于取余,同时解决了head为负值的情况。因为elements.length必需是2的指数倍,elements - 1就是二进制低位全1,跟head - 1相与之后就起到了取模的作用,如果head - 1为负数(其实只可能是-1),则相当于对其取相对于elements.length的补码。
计算机里数值都是用补码表示的,如果是8位的,-1就是1111 1111,而 (elements.length - 1) 也是 1111 1111,因此两者相与也就是(elements.length - 1);
head = (head - 1) & (elements.length - 1) 最后再让算出的位置赋值给head,因此其实这段代码就是让head再从后往前赋值
扩容函数doubleCapacity(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:
图中可以看到,复制分两次进行,第一次复制head右边的元素,第二次复制head左边的元素。
//doubleCapacity()
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // head右边元素的个数
int newCapacity = n << 1;//原空间的2倍
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分
System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分
elements = (E[])a;
head = 0;
tail = n;
}
addLast()
addLast(E e)的作用是在Deque的尾端插入元素,也就是在tail的位置插入元素,由于tail总是指向下一个可以插入的空位,因此只需要elements[tail] = e;即可。插入完成后再检查空间,如果空间已经用光,则调用doubleCapacity()进行扩容。
public void addLast(E e) {
if (e == null)//不允许放入null
throw new NullPointerException();
elements[tail] = e;//赋值
if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下标越界处理
doubleCapacity();//扩容
}
pollFirst()
pollFirst()的作用是删除并返回Deque首端元素,也即是head位置处的元素。如果容器不空,只需要直接返回elements[head]即可,当然还需要处理下标的问题。由于ArrayDeque中不允许放入null,当elements[head] == null时,意味着容器为空。
public E pollFirst() {
int h = head;
E result = elements[head];
if (result == null)//null值意味着deque为空
return null;
elements[h] = null;//let GC work
head = (head + 1) & (elements.length - 1);//下标越界处理
return result;
}
pollLast()
pollLast()的作用是删除并返回Deque尾端元素,也即是tail位置前面的那个元素。
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);//tail的上一个位置是最后一个元素
E result = elements[t];
if (result == null)//null值意味着deque为空
return null;
elements[t] = null;//let GC work
tail = t;
return result;
}
peekFirst()
peekFirst()的作用是返回但不删除Deque首端元素,也即是head位置处的元素,直接返回elements[head]即可。
public E peekFirst() {
return elements[head]; // elements[head] is null if deque empty
}
peekLast()
peekLast()的作用是返回但不删除Deque尾端元素,也即是tail位置前面的那个元素。
public E peekLast() {
return elements[(tail - 1) & (elements.length - 1)];
}
关于作者
来自一线程序员Seven的探索与实践,持续学习迭代中~
本文已收录于我的个人博客:https://www.seven97.top
公众号:seven97,欢迎关注~
为什么Java已经不推荐使用Stack了?的更多相关文章
- JAVA程序员必看的15本书-JAVA自学书籍推荐
作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...
- java学习书籍推荐
1. Java 语言基础 谈到Java 语言基础学习的书籍,大家肯定会推荐Bruce Eckel 的<Thinking in Java >.它是一本写的相当深刻的技术书籍,Java 语言基 ...
- 最强 Java 书单推荐,附学习方法
技术大佬用1w+字来告诉你该读什么书,循序渐进,并提供百度云盘下载地址.重要的是还有学习方法. 请肆无忌惮地点赞吧,微信搜索[沉默王二]关注这个在九朝古都洛阳苟且偷生的程序员.本文 GitHub gi ...
- java后端书籍推荐
书架主要针对Java后端开发. 3.0版把一些后来买的.看的书添补进来,又或删掉或降级一些后来没有再翻开过的书. 更偏爱那些能用简短流畅的话,把少壮不努力的程序员所需的基础补回来的薄书,而有些教课书可 ...
- 10本Java经典书目推荐
本文列出的10本书是我个人非常喜欢的Java书籍,当我有时间的时候,我就会将它们捧在手里阅读.甚至有些书我反复读过很多遍,每次重新读的时候总会有新的收获.因此这些书也是大部分Java程序员喜欢的书籍. ...
- Java 集合系列07之 Stack详细介绍(源码解析)和使用示例
概要 学完Vector了之后,接下来我们开始学习Stack.Stack很简单,它继承于Vector.学习方式还是和之前一样,先对Stack有个整体认识,然后再学习它的源码:最后再通过实例来学会使用它. ...
- Java for LeetCode 225 Implement Stack using Queues
Implement the following operations of a stack using queues. push(x) -- Push element x onto stack. po ...
- Java开源框架推荐(全)
Build Tool Tools which handle the buildcycle of an application. Apache Maven - Declarative build and ...
- Java for LeetCode 155 Min Stack
Design a stack that supports push, pop, top, and retrieving the minimum element in constant time. pu ...
- java进阶书籍推荐
第一部分: Java语言篇 1 <Java编程规范> 星级: 适合对象:初级,中级 介绍:作者James Gosling(Java之父),所以这本书我觉得你怎么也得读一下.对基 ...
随机推荐
- Mysql中where条件自动类型转换的坑
我有张表,其主键id字段为varchar(5),内容是5位随机不重复字符串表的内容大概是这样的 id name s8bk2 admin 9f0ps username 在一个方法中我查询了这张表,代码大 ...
- [oeasy]python0066_控制序列_光标位置设置_ESC_逃逸字符_CSI
光标位置 回忆上次内容 上次讲了 三引号的输出 三引号中 回车和引号 都会 被原样输出 \ 还是需要从 \\转义 黑暗森林 快被摸排清了 还有哪个 转义序列 没 研究过吗? \e是 干什么的? 回忆转 ...
- 二分查找 | C++
以此题为例:P2249 [深基13.例1]查找 二分查找 对于一个单调不降的序列 \(S\),传统查找的复杂度是 \(O(|S|)\),即 \(O(n)\). 有时候序列 \(S\) 中的元素特别多, ...
- OpenGL之ShadowMap
流程:先创建一个RenderTexture,然后用灯光的视口渲染. 然后切换到正常相机,进行渲染,使用RenderTexture中的深度或者颜色纹理,然后还原当前顶点在灯光中的深度,两者对比,比缓存中 ...
- git篇-- Git在项目实操中常见的使用命令--02
Git是现代软件开发中不可或缺的版本控制工具.它能帮助开发者跟踪项目的所有变更,并与团队成员高效协作.本文将介绍一些在项目实操中常见的Git命令,帮助你更好地管理代码. 1. 初始化和配置 初始化仓库 ...
- Windows下搭建Vue脚手架CLI
Vue CLI的使用依赖Node.js,先按照Node.js环境. //安装环境C:\Users\16779>npm install --global vue-cli npm WARN depr ...
- 平衡树之Splay树详解
认识 Splay树,BST(二叉搜索树)的一种,整体效率很高,平摊操作次数为\(O(log_2n)\),也就是说,在一棵有n个节点的BST上做M次Splay操作,时间复杂度为\(O(Mlog_2n)\ ...
- (二)MongoDB的在SpringBoot中的应用
我来填之前MongoDB的坑了,项目中又用到MongoDB的我又想起来了,我这拖延症也是没谁了. 1.在pom.xml中引入依赖 <dependency> <groupId>o ...
- 【Vue2】金额范围查询项
Element 只提供了DatePicker,没有做金额的Picker 这个东西就只能自己做了,实现效果: 后台接口条件: 因为有可能只有起始值,只有结束值,或者起始值结束值都有三种情况 1.如果有起 ...
- 【BatchProgram】 读取文本批量创建目录
NameList.txt文件内容 FILE-NAME-A FILE-NAME-B FILE-NAME-C ... 根据上面文件批量创建对应的目录,且附加序号 CMD代码: @ECHO OFF SETL ...