花瓣小跑 发表于 2024-9-10 21:10:52

为什么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,那应该使用什么集合类来替换栈,一起看看官方的文档。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856058.jpg
正如图中标注部分所示,栈的相关操纵应该由 Deque 接口来提供,推荐使用 Deque 这种数据结构, 以及它的子类,比方 ArrayDeque。
val stack: Deque<Int> = ArrayDeque()使用 Deque 接口来实现栈的功能有什么好处:

[*]速度比 Stack 快
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856079.jpg
这个类作为栈使用时可能比 Stack 快,作为队列使用时可能比 LinkedList 快。因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。

[*]屏蔽掉无关的方法
原来的 Java 的 Stack,包含了在任何位置添加或者删除元素的方法,这些不是栈应该有的方法,所以须要屏蔽掉这些无关的方法。声明为 Deque 接口可以解决这个问题,在接口中声明栈须要用到的方法,无需管子类是如何是实现的,对于上层使用者来说,只可以调用和栈相关的方法。
Stack 和 ArrayDeque的 区别

集合范例数据结构是否线程安全Stack数组是ArrayDeque数组否Stack 常用的方法如下所示:
操纵方法入栈push(Eitem)出栈pop()查看栈顶peek() 为空时返回 nullArrayDeque 常用的方法如下所示:
操纵方法入栈push(Eitem)出栈poll() 栈为空时返回    nullpop() 栈为空时会抛出异常查看栈顶peek() 为空时返回 nullQueue介绍

Java里有一个叫做Stack的类,却没有叫做Queue的类(它是个接口名字)。当须要使用栈时,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque;既然Queue只是一个接口,当须要使用队列时也就首选ArrayDeque了(次选是LinkedList)。
Queue

Queue接口继承自Collection接口,除了最根本的Collection的方法之外,它还支持额外的insertion, extraction和inspection操纵。这里有两组格式,共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856302.gif
Deque

Deque 是"double ended queue", 表示双向的队列,英文读作"deck". Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持 insert , remove 和 examine操纵,由于Deque是双向的,所以可以对队列的头和尾都进行操纵,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。共12个方法如下:
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856307.gif
当把 Deque 当做FIFO的 queue 来使用时,元素是从 deque 的尾部添加,从头部进行删除的; 所以 deque 的部分方法是和 queue 是等同的。具体如下:
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856311.gif
Deque的寄义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了Deque与Queue相对应的接口:
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856322.gif
下表列出了Deque与Stack对应的接口:
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856314.gif
上面两个表共定义了Deque的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理差别。一套接口碰到失败就会抛出异常,另一套碰到失败会返回特殊值( false 或 null )。除非某种实现对容量有限定,大多数情况下,添加操纵是不会失败的。固然Deque的接口有12个之多,但无非就是对容器的两端进行操纵,或添加,或删除,或查看。
ArrayDeque和LinkedList是Deque的两个通用实现,由于官方更推荐使用AarryDeque用作栈和队列,加之上一篇已经讲解过LinkedList,本文将偏重讲解ArrayDeque的具体实现
从名字可以看出ArrayDeque底层通过数组实现,为了满意可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者止境。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,须要程序员手动同步;另外,该容器不允许放入 null 元素。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856328.jpg
上图中我们看到, head 指向首端第一个有效元素, tail 指向尾端第一个可以插入元素的空位。因为是循环数组,所以 head 不一定总等于0, tail 也不一定总是比 head 大。
方法分析

addFirst()

addFirst(E e)的作用是在Deque的首端插入元素,也就是在head的前面插入元素,在空间足够且下标没有越界的情况下,只须要将elements[--head] = e即可。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856996.jpg
实际须要考虑:

[*]空间是否够用
[*]下标是否越界的问题
上图中,假如head为0之后接着调用addFirst(),固然空余空间还够用,但head为-1,下标越界了。
//addFirst(E e)
public void addFirst(E e) {
    if (e == null)//不允许放入null
      throw new NullPointerException();
    elements = 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(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404250856017.jpg
图中可以看到,复制分两次进行,第一次复制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]
查看完整版本: 为什么Java已经不推荐使用Stack了?