滚雪球学Java(18):解密JavaSE中的堆栈:你真的了解Java内存吗?

简介: 【4月更文挑战第7天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

在这里插入图片描述


🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!!


前言

  在 Java 编程中,堆栈是非常常见的数据结构。堆栈是一种线性数据结构,它具有后进先出 (Last In First Out, LIFO) 的特性。在 Java 中,堆栈可以使用数组或链表实现。本文旨在介绍 Java 的堆栈的实现方式,并提供一些相关的代码示例。

摘要

  本文主要介绍了 Java 中堆栈的实现方式以及相关的代码示例。首先,我们介绍了堆栈的基本概念以及其操作。接着,我们分别介绍了使用数组和链表实现堆栈的方法,并提供了相应的代码示例。最后,我们总结了本文的内容,并提出了一些进一步的思考。

正文

1. 堆栈的基本概念和操作

  堆栈是一种线性数据结构,它具有后进先出 (Last In First Out, LIFO) 的特性。堆栈通常支持两种基本操作:

  • 入栈 (push):将一个元素放入堆栈顶部;
  • 出栈 (pop):将堆栈顶部的元素移除;

  除此之外,堆栈还可以支持一些其他的操作,例如:

  • 获取栈顶元素 (peek):查看堆栈顶部的元素,但不将其移除;
  • 判断堆栈是否为空 (isEmpty):判断堆栈是否为空;
  • 获取堆栈中元素的个数 (size):获取堆栈中元素的个数;

2. 使用数组实现堆栈

  使用数组实现堆栈非常简单,我们只需要定义一个数组和一个指针,指针指向堆栈顶部元素的下一个位置。入栈操作就是将元素放入数组的当前指针位置,然后指针加一;出栈操作就是将指针减一,然后返回当前指针位置的元素。

public class ArrayStack<E> {
   

    private int top; // 栈顶指针
    private E[] array;

    public ArrayStack(int capacity) {
   
        array = (E[]) new Object[capacity];
        top = 0;
    }

    public void push(E element) {
   
        if (top >= array.length) {
   
            throw new StackOverflowError();
        }
        array[top++] = element;
    }

    public E pop() {
   
        if (isEmpty()) {
   
            throw new EmptyStackException();
        }
        return array[--top];
    }

    public E peek() {
   
        if (isEmpty()) {
   
            throw new EmptyStackException();
        }
        return array[top - 1];
    }

    public boolean isEmpty() {
   
        return top == 0;
    }

    public int size() {
   
        return top;
    }
}
AI 代码解读

  上面的代码中,我们使用泛型来定义了 ArrayStack 类,它可以存储任意类型的元素。在构造方法中,我们创建了一个指定容量的数组和一个初始值为 0 的栈顶指针。在 push 方法中,如果栈已满,就抛出一个 StackOverflowError 异常;否则,就将元素放入数组当前指针位置,然后指针加一。在 pop 方法中,如果栈为空,就抛出一个 EmptyStackException 异常;否则,就将指针减一,然后返回当前指针位置的元素。peek、isEmpty 和 size 方法也是类似的实现。

拓展:

  此代码实现了一个基于数组的栈结构。具体分析如下:

  1. 类定义:public class ArrayStack<E> 表示这是一个泛型类,使用泛型类型参数 E 表示栈中元素的类型。

  2. 私有字段:private int top; 表示栈顶指针,标识当前栈顶元素的位置。private E[] array; 表示存储栈元素的数组。

  3. 构造方法:public ArrayStack(int capacity) 接受一个整型参数 capacity,表示栈的容量。在构造方法内部,通过 (E[]) new Object[capacity] 创建了一个数组,并将其赋值给 array 字段。此处使用类型转换 (E[]) 是因为 Java 的泛型不允许直接创建泛型数组。

  4. push(E element) 方法:将元素 element 入栈。首先检查栈是否已满,即 top >= array.length,如果已满则抛出 StackOverflowError 异常。否则,将 element 存储到 array[top],并将 top 指针向上移动一位。

  5. pop() 方法:出栈操作,即移除并返回栈顶元素。首先检查栈是否为空,即 isEmpty() 方法返回 true,如果为空则抛出 EmptyStackException 异常。否则,将 top 指针向下移动一位,并返回 array[top]

  6. peek() 方法:返回栈顶元素,但不移除它。首先检查栈是否为空,即 isEmpty() 方法返回 true,如果为空则抛出 EmptyStackException 异常。否则,返回 array[top - 1]

  7. isEmpty() 方法:判断栈是否为空,即 top == 0

  8. size() 方法:返回栈的元素个数,即 top 的值。

  需要注意的是,该代码实现没有对栈的容量进行动态扩容,如果栈满时继续入栈会抛出 StackOverflowError 异常。在使用时需要注意栈的容量是否足够。还有,由于使用了泛型,需要在创建栈对象时传入具体的元素类型参数。

3. 使用链表实现堆栈

  使用链表实现堆栈也是一种常见的方式。链表的头部代表堆栈顶部元素,因此入栈操作就是在链表头部插入元素,出栈操作就是从链表头部移除元素。

public class LinkedStack<E> {
   

    private Node top; // 栈顶节点
    private int size; // 元素个数

    private class Node {
   
        E element;
        Node next;

        public Node(E element, Node next) {
   
            this.element = element;
            this.next = next;
        }
    }

    public void push(E element) {
   
        top = new Node(element, top);
        size++;
    }

    public E pop() {
   
        if (isEmpty()) {
   
            throw new EmptyStackException();
        }
        E element = top.element;
        top = top.next;
        size--;
        return element;
    }

    public E peek() {
   
        if (isEmpty()) {
   
            throw new EmptyStackException();
        }
        return top.element;
    }

    public boolean isEmpty() {
   
        return top == null;
    }

    public int size() {
   
        return size;
    }
}
AI 代码解读

  上面的代码中,我们定义了一个 Node 类作为链表的节点,每个节点包含一个元素和一个指向下一个节点的指针。在类中,我们定义了一个头节点 top 和一个元素个数 size。在 push 方法中,我们创建一个新的节点,并将它作为新的头节点;在 pop 方法中,我们移除当前头节点,并将下一个节点作为新的头节点。peek、isEmpty 和 size 方法也是类似的实现。

拓展:

  该代码实现了一个基于链表的栈数据结构。栈是一种特殊的线性数据结构,具有先进后出(LIFO)的特点。

  该类中有两个内部类:Node和LinkedStack。Node类用于表示栈中的节点,保存元素和下一个节点的引用;LinkedStack类是栈的实现类,包括栈顶节点和元素个数。

代码中的主要方法包括:

  1. push方法:将元素压入栈顶。创建一个新的节点,将该节点设置为栈顶节点的下一个节点,并将栈顶节点更新为新节点。同时,元素个数加一。

  2. pop方法:弹出栈顶元素。如果栈为空,则抛出EmptyStackException异常。否则,获取栈顶节点的元素,并将栈顶节点更新为其下一个节点。同时,元素个数减一。

  3. peek方法:返回栈顶元素,但不弹出。如果栈为空,则抛出EmptyStackException异常。

  4. isEmpty方法:判断栈是否为空。如果栈顶节点为null,则认为栈为空。

  5. size方法:返回栈中元素的个数。

  这个实现基于链表的栈相比于基于数组的栈,具有动态性,可以根据实际情况调整栈的大小。但也因此牺牲了一些性能,如访问元素的时间复杂度在最坏情况下为O(n),而不是O(1)。

  需要注意的是,该代码中使用了泛型E表示元素的类型,可以为任意类型。

4. 测试用例

为了验证我们的实现是否正确,我们编写了以下测试用例:

@Test
public void testArrayStack() {
   
    ArrayStack<Integer> stack = new ArrayStack<>(10);
    assertTrue(stack.isEmpty());
    assertEquals(0, stack.size());

    stack.push(1);
    stack.push(2);
    stack.push(3);
    assertFalse(stack.isEmpty());
    assertEquals(3, stack.size());
    assertEquals(3, (int) stack.pop());
    assertEquals(2, (int) stack.peek());
    assertEquals(2, (int) stack.pop());
    assertEquals(1, (int) stack.pop());

    assertThrows(EmptyStackException.class, stack::pop);
    assertThrows(EmptyStackException.class, stack::peek);
}

@Test
public void testLinkedStack() {
   
    LinkedStack<Integer> stack = new LinkedStack<>();
    assertTrue(stack.isEmpty());
    assertEquals(0, stack.size());

    stack.push(1);
    stack.push(2);
    stack.push(3);
    assertFalse(stack.isEmpty());
    assertEquals(3, stack.size());
    assertEquals(3, (int) stack.pop());
    assertEquals(2, (int) stack.peek());
    assertEquals(2, (int) stack.pop());
    assertEquals(1, (int) stack.pop());

    assertThrows(EmptyStackException.class, stack::pop);
    assertThrows(EmptyStackException.class, stack::peek);
}
AI 代码解读

  在测试用例中,我们先分别创建了一个数组栈和一个链表栈,并进行了一系列 push、pop、peek、isEmpty 和 size 操作的验证。最后,我们使用 assertThrows 方法验证了在栈为空时,pop 和 peek 操作是否会抛出 EmptyStackException 异常。

拓展:

  这段代码是对栈数据结构进行单元测试的代码。

  首先,定义了两个测试方法:testArrayStack()testLinkedStack()。分别对基于数组和链表实现的栈进行测试。

  在testArrayStack()方法中,首先创建了一个容量为10的ArrayStack对象,并进行了一系列断言验证。验证了栈的初始状态应该为空,大小为0。

  然后,通过push()方法向栈中添加了三个元素:1,2和3。进行了一系列断言验证:栈不为空,大小为3。然后依次进行了pop()peek()操作,并进行了相应的断言验证。

  最后,使用assertThrows()方法验证了在栈为空时进行pop()peek()操作会抛出EmptyStackException异常。

  testLinkedStack()方法与testArrayStack()方法的逻辑类似,不过是使用了LinkedStack类来进行测试。

5. 小结

  本文介绍了 Java 中堆栈的基本概念和操作,以及使用数组和链表分别实现堆栈的方法。我们还提供了相应的代码示例和测试用例。在实际编程中,我们可以根据实际情况选择不同的堆栈实现方式。在使用堆栈时,我们需要确保堆栈中的元素满足后进先出的原则。

总结

  本文介绍了 Java 中堆栈的实现方式以及基本概念和操作。在 Java 中,堆栈是一种非常常见的数据结构,它具有后进先出 (Last In First Out, LIFO) 的特性。堆栈通常支持入栈、出栈、获取栈顶元素、判断堆栈是否为空以及获取堆栈中元素个数等基本操作。

  在 Java 中,我们可以使用数组或链表来实现堆栈。使用数组实现堆栈非常简单,我们只需要定义一个数组和一个指针,指针指向堆栈顶部元素的下一个位置。入栈操作就是将元素放入数组的当前指针位置,然后指针加一;出栈操作就是将指针减一,然后返回当前指针位置的元素。使用链表实现堆栈也是一种常见的方式,链表的头部代表堆栈顶部元素。入栈操作就是在链表头部插入元素,出栈操作就是从链表头部移除元素。

  在实际编程中,我们可以根据实际需求选择不同的堆栈实现方式。使用数组实现的堆栈通常需要指定一个固定的容量,而链表实现的堆栈可以根据需要动态扩展。无论使用哪种实现方式,我们都需要确保堆栈中的元素满足后进先出的原则。

  最后,我们编写了相应的测试用例来验证数组和链表实现的堆栈是否正常工作。在编写测试用例时,我们对入栈、出栈、获取栈顶元素、判断堆栈是否为空以及获取堆栈中元素个数等操作进行了验证。

  ... ...

  好啦,这期的内容就基本接近尾声啦,若你想学习更多,你可以看看专栏的导读篇《「滚雪球学Java」教程导航帖》,本专栏致力打造最硬核 Java 零基础系列学习内容,🚀打造全网精品硬核专栏,带你直线超车;欢迎大家订阅持续学习。功不唐捐,久久为功!

「赠人玫瑰,手留余香」,咱们下期拜拜~~

附录源码

  如上涉及所有源码均已上传同步在「Gitee」,提供给同学们一对一参考学习,辅助你更迅速的掌握。

☀️建议/推荐你

  无论你是计算机专业的学生,还是对编程感兴趣的跨专业小白,都建议直接入手「滚雪球学Java」专栏;该专栏不仅免费,bug菌还郑重承诺,只要你学习此专栏,均能入门并理解Java SE,以全网最快速掌握Java语言,每章节源码均同步「Gitee」,你真值得拥有;学习就像滚雪球一样,越滚越大,带你指数级提升。

  码字不易,如果这篇文章对你有所帮助,帮忙给bugj菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。

  同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!


目录
打赏
0
1
1
0
207
分享
相关文章
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
160 29
JVM简介—1.Java内存区域
【YashanDB知识库】kettle同步大表提示java内存溢出
在数据导入导出场景中,使用Kettle进行大表数据同步时出现“ERROR:could not create the java virtual machine!”问题,原因为Java内存溢出。解决方法包括:1) 编辑Spoon.bat增大JVM堆内存至2GB;2) 优化Kettle转换流程,如调整批量大小、精简步骤;3) 合理设置并行线程数(PARALLELISM参数)。此问题影响所有版本,需根据实际需求调整相关参数以避免内存不足。
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
70 0
|
2月前
|
java设置栈内存大小
在Java应用中合理设置栈内存大小是确保程序稳定性和性能的重要措施。通过JVM参数 `-Xss`,可以灵活调整栈内存大小,以适应不同的应用场景。本文介绍了设置栈内存大小的方法、应用场景和注意事项,希望能帮助开发者更好地管理Java应用的内存资源。
78 4
【YashanDB 知识库】kettle 同步大表提示 java 内存溢出
【问题分类】数据导入导出 【关键字】数据同步,kettle,数据迁移,java 内存溢出 【问题描述】kettle 同步大表提示 ERROR:could not create the java virtual machine! 【问题原因分析】java 内存溢出 【解决/规避方法】 ①增加 JVM 的堆内存大小。编辑 Spoon.bat,增加堆大小到 2GB,如: if "%PENTAHO_DI_JAVA_OPTIONS%"=="" set PENTAHO_DI_JAVA_OPTIONS="-Xms512m" "-Xmx512m" "-XX:MaxPermSize=256m" "-
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
89 0
JVM实战—2.JVM内存设置与对象分配流转
本文详细介绍了JVM内存管理的相关知识,包括:JVM内存划分原理、对象分配与流转、线上系统JVM内存设置、JVM参数优化、问题汇总。
JVM实战—2.JVM内存设置与对象分配流转
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
1001 1

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等