目录
[<1> 入栈 push](#<1> 入栈 push)
[<2> 出栈 pop](#<2> 出栈 pop)
[<3> 获取栈顶元素 peek](#<3> 获取栈顶元素 peek)
[<4> 获取栈中有效元素个数 size](#<4> 获取栈中有效元素个数 size)
[<5> 检测栈是否为空 isEmpty](#<5> 检测栈是否为空 isEmpty)
一,前言
栈是一种特殊的线性存储结构,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出的原则,即后面入栈的元素先出栈。
栈根据实现方式不同又分为链式栈和线性栈,本文演示的为链式栈。栈的具体操作如下图所示:
二,基本实现
1,接口定义
本次实现选择传递泛型,故接口类也写作泛型类。
java
public interface IStack <T> {
// 入栈
T push(T val);
// 出栈
T pop();
// 获取栈顶元素
T peek();
// 获取栈中有效元素个数
int size();
// 检测栈是否为空
boolean isEmpty();
}
2,实现类定义
在本次实现中,使用无头非循环单链表作为基本结构,定义first引用记录头节点,定义usedSize基础变量记录栈中有效元素个数。
使用单链表作为基本结构是因为栈只需要在一端进行插入或删除,而单链表的头删和头插正好契合需求,该选型可让时间复杂度可达到O(1)并且无需扩容。
java
public class StackImpl<T> implements IStack<T>{
private Node<T> first;
private int usedSize;
/**
* 无参构造方法*/
public StackImpl() {
this.first = null;
this.usedSize = 0;
}
/**
* 节点内部类(使用单链表)*/
private static class Node<T> {
T val;
Node<T> next;
public Node(T val) {
this.val = val;
}
}
}
3,主要方法实现
<1> 入栈 push
入栈的逻辑实际上就是单链表的头插,如下图所示:

java
/**
* 入栈*/
@Override
public T push(T val) {
// 创建新节点
Node<T> node = new Node<>(val);
// 入栈
node.next = this.first;
this.first = node;
// 维护栈有效元素个数
this.usedSize ++;
return val;
}
<2> 出栈 pop
出栈时存在两种情况:
1,栈为空 ==> 抛出异常
2,栈不为空 ==> 头删,如下图所示:

java
/**
* 出栈*/
@Override
public T pop() {
// 判空
if(isEmpty()) {
throw new StackEmptyException("不可对空栈执行出栈操作!");
} else {
T oldVal = this.first.val;
this.first = this.first.next;
// 维护栈有效元素个数
this.usedSize --;
return oldVal;
}
}
<3> 获取栈顶元素 peek
获取栈顶元素时存在两种情况:
1,栈为空 ==> 抛出异常
2,栈不为空 ==> 返回头节点元素
java
/**
* 获取栈顶元素*/
@Override
public T peek() {
// 判空
if(isEmpty()) {
throw new StackEmptyException("不可对空栈执行获取栈顶元素操作!");
} else {
return this.first.val;
}
}
三,栈、虚拟机栈、栈帧之间的区别
直白的说,栈是一种数据结构,虚拟机栈是 JVM 划分的一块线程私有内存,而栈帧是调用方法时,在虚拟机栈内部为该方法开辟的内存单元。