为什么不推荐使用Stack

news2024/12/23 1:42:57

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即可。

实际需要考虑:

  1. 空间是否够用
  2. 下标是否越界的问题

上图中,如果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)];
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2128133.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鸟类目标检测系统源码分享

鸟类目标检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

亲测好用,ChatGPT 3.5/4.0新手使用手册~

都知道ChatGPT很强大&#xff0c;聊聊天、写论文、搞翻译、写代码、写文案、审合同等等&#xff0c;无所不能~ 那么到底怎么使用呢&#xff1f;其实很简单了&#xff0c;国内AI产品发展也很快&#xff0c;很多都很好用了~ 我一直在用&#xff0c;建议收藏下来~ 有最先进、最…

RocketMQ出现The broker does not support consumer to filter message by SQL92

在使用RocketMQ使用SQL过滤消息的时候&#xff0c;出现下面错误 原因是我们的配置文件没有开启SQL过滤功能&#xff0c;我们需要在每个配置文件中添加下面命令 #开启过滤消息时支持SQL92标准 enablePropertyFiltertrue接着我们重启namesrv与broker服务就解决问题 # 1.进入bi…

Robust Image Denoising through Adversarial Frequency Mixup

基于对抗性混频的鲁棒图像去噪 论文链接&#xff1a;https://openaccess.thecvf.com/CVPR2024/Ryou_Robust_Image_Denoising_through_Adversarial_Frequency_Mixup 项目链接&#xff1a;https://github.com/dhryougit/AFM Abstract 基于深度神经网络的图像去噪方法经常与训练…

哈希表的底层实现(1)---C++版

目录 哈希表的基本原理 哈希表的优点 哈希表的缺点 应用场景 闭散列法 开散列法 开放定值法Open Addressing——线性探测的模拟实现 超大重点部分评析 链地址法Separate Chaining——哈希桶的模拟实现 哈希表&#xff08;Hash Table&#xff09;是一种数据结构&#x…

STM32G070 CubeMX配置多通道/单通道ADC+DMA流程 LL库

基础配置不再赘述&#xff0c;时钟这些根据硬件来配置 多通道ADCDMA配置图&#xff1a; 程序配置&#xff1a; 调试查看内存数据&#xff0c;硬件上将PA1接到GND&#xff0c;PA2接到3V3 采集的数据会循环覆盖内存 问题&#xff1a;代码里先初始化ADC_IN1&#xff0c;再初…

Spring扩展点系列-ApplicationContextAwareProcessor

文章目录 简介源码分析示例代码示例一&#xff1a;扩展点的执行顺序运行示例一 示例二&#xff1a;获取配置文件值配置文件application.properties内容定义工具类ConfigUtilcontroller测试调用运行示例二 示例三&#xff1a;实现ResourceLoaderAware读取文件ExtendResourceLoad…

CleanClip - 「CleanClip」是一款专为 Mac 设计的桌面剪贴板工具

官方介绍 欢迎使用 CleanClip —— Mac 上最简洁高效的剪贴板管理工具。CleanClip 专为追求简约操作体验的用户设计&#xff0c;它帮助用户记录系统剪贴板上的内容&#xff0c;并提供强大的分类管理能力&#xff0c;帮助你整理复制的内容&#xff0c;提高办公效率。 智能简洁&…

MAVEN如何导入项目

工作中经常需要导入他人的项目&#xff0c;那么如何导入呢&#xff1f; 1&#xff0c; 选择Maven面板&#xff0c;点 2&#xff0c;选中对应项目的pom.xml&#xff0c;双击即可 3&#xff0c;如果没有maven面板&#xff0c;可以选择view->Appearnce->Tool Window Bars…

HTML5元素定位

1.元素定位 为了实现网页整体布局&#xff0c;我们先要知道&#xff0c;一个元素&#xff0c;是如何定位到页面上的某个位置的&#xff0c;这就是元素定位。 元素定位有四种&#xff0c;可以使用position样式来设置元素定位&#xff0c;所以此属性值有四种&#xff1a; stat…

MybatisPlus新增数据时怎么返回新增数据的id

问&#xff1a;MybatisPlus新增数据时怎么返回新增数据的id&#xff1f;答&#xff1a;当插入操作执行后&#xff0c;MyBatis Plus会自动获取生成的ID并将其设置到传入的实体类对象的id属性中。当然&#xff0c;这需要你的表字段ID是自增的 实体类代码 public class Sites {p…

东风德纳携手纷享销客打造汽车零部件行业营销数智化新标杆

为进一步提升数字化经营管理水平&#xff0c;加速数字化转型&#xff0c;推进“品牌向上”战略落实落地&#xff0c;9月2日&#xff0c;东风德纳车桥有限公司召开CRM项目启动会&#xff0c;携手纷享销客&#xff0c;打造汽车零部件行业营销数智化标杆工程。东风德纳车桥总经理陆…

高效Flutter应用开发:GetX状态管理实战技巧

探索GetX状态管理的使用 前言 在之前的文章中&#xff0c;我们详细介绍了 Flutter 应用中的状态管理&#xff0c;setState、Provider库以及Bloc的使用。 本篇我们继续介绍另一个实现状态管理的方式&#xff1a;GetX。 一、GetX状态管理 基础介绍 GetX 是一个在 Flutter 中…

【原创】【总结】【C++类的设计要点】一道十分典型的含继承与虚函数的类设计题

设计类时的要点 1构造函数与析构函数&#xff1a;先在public中写上构造函数与析构函数 2成员函数&#xff1a;根据题目要求在public中声明成员函数&#xff1b;成员函数的实现在类内类外均可&#xff0c;注意若在类外实现时用::符号表明是哪个类的函数 3数据成员&#xff1a;关…

STM32L051K8U6-HAL-串口中断控制灯闪烁速度

HAL三步法&#xff1a; 1、配置下载线 2、配置晶振 3、配置时钟 4、 配置灯引脚属性为输出模式。并设置标签为LED 5、配置串口1 串口常用函数说明&#xff1a; 需要实现的伪代码&#xff1a; 示例&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1u6FamKgZhvcEsFAdgGeaw…

Realsense D455 imu 数据不输出?

现象 realsense_viewer 可以可视化查看imu数据, 但是realsense-ros 查看/camera/accel/sample和/camera/gyro/sample没有数据输出 背景 realsense_viewer 安装: sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCDE…

移动通信为啥要用双极化天线?

❝本文简单介绍下移动通信为啥要用双极化天线及其简单概述。 移动通信为啥要用双极化天线&#xff1f; - RFASK射频问问❝本文简单介绍下移动通信为啥要用双极化天线及其简单概述。什么是极化&#xff1f;电磁波的极化通常是用其电场矢量的空间指向来描述&#xff1a;在空间某…

Leetcode 字母异位词分组

这道题目的意思就是&#xff1a;把包含字母字符相同的单词分到同一组。 算法思路&#xff1a; 使用哈希表来解决。 首先将每个字符串进行排序&#xff0c;将排序之后的字符串作为 key&#xff0c;然后将用 key 所对应的异位词组 作为value。然后我们使用 std::pair 来遍历 键…

Vue的学习(三)

目录 一、for循环中key的作用 1‌.提高性能‌&#xff1a; ‌2.优化用户体验‌&#xff1a; ‌3.辅助Vue进行列表渲染‌&#xff1a; 4‌.方便可复用组件的使用‌&#xff1a; 二、methods及computed及wacth的区别 三、过滤器 1.Vue 2 过滤器简介 定义过滤器 使用过滤…

八、适配器模式

适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许不兼容的接口之间进行合作。适配器模式通过创建一个适配器类来转换一个接口的接口&#xff0c;使得原本由于接口不兼容无法一起工作的类可以一起工作。 主要组成部分&#xff1a; 目标…