A stack is a linear data structure that operates on a Last-In, First-Out (LIFO) principle. This means the last element added to the stack is the first one to be removed. It is analogous to a stack of plates where you add new plates on top and take them off from the top as well. Stacks are used in various applications like parsing expressions, managing function calls, and implementing undo operations.
Java provides several ways to implement a stack, including using arrays, linked lists, and the Java Collections Framework.
An array-based stack uses a fixed-size array to hold elements. Although it’s easy to implement, it doesn’t dynamically resize and hence might lead to stack overflow or underflow.
class ArrayStack {
private int top;
private int[] stack;
private int capacity;
public ArrayStack(int capacity) {
this.capacity = capacity;
stack = new int[capacity];
top = -1;
}
public boolean isEmpty() {
return top == -1;
}
public boolean isFull() {
return top == capacity - 1;
}
public void push(int value) {
if (isFull()) {
System.out.println("Stack Overflow");
return;
}
stack[++top] = value;
}
public int pop() {
if (isEmpty()) {
System.out.println("Stack Underflow");
return Integer.MIN_VALUE;
}
return stack[top--];
}
public int peek() {
if (isEmpty()) {
System.out.println("Stack is empty");
return Integer.MIN_VALUE;
}
return stack[top];
}
}
A linked list-based stack uses a linked list to store elements, providing dynamic resizing. Each node contains data and a reference to the next node.
class LinkedListStack {
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
next = null;
}
}
private Node top;
public LinkedListStack() {
top = null;
}
public boolean isEmpty() {
return top == null;
}
public void push(int value) {
Node newNode = new Node(value);
newNode.next = top;
top = newNode;
}
public int pop() {
if (isEmpty()) {
System.out.println("Stack Underflow");
return Integer.MIN_VALUE;
}
int value = top.data;
top = top.next;
return value;
}
public int peek() {
if (isEmpty()) {
System.out.println("Stack is empty");
return Integer.MIN_VALUE;
}
return top.data;
}
}
Java’s Stack class, which extends Vector, is a built-in class that provides stack operations with dynamic resizing. It also provides several utility methods to make stack operations easier.
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Top element: " + stack.peek());
System.out.println("Stack size: " + stack.size());
System.out.println("Popping element: " + stack.pop());
System.out.println("After pop, top element: " + stack.peek());
}
}
In multi-threaded environments, stacks need to be thread-safe to prevent data corruption. Java’s ConcurrentLinkedDeque provides a thread-safe stack-like structure
import java.util.concurrent.ConcurrentLinkedDeque;
public class ConcurrentStackExample {
public static void main(String[] args) {
ConcurrentLinkedDeque<Integer> stack = new ConcurrentLinkedDeque<>();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Top element: " + stack.peek());
System.out.println("Stack size: " + stack.size());
System.out.println("Popping element: " + stack.pop());
System.out.println("After pop, top element: " + stack.peek());
}
}
Deque (double-ended queue) supports both stack and queue operations, allowing elements to be added or removed from both ends.
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeStackExample {
public static void main(String[] args) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Top element: " + stack.peek());
System.out.println("Stack size: " + stack.size());
System.out.println("Popping element: " + stack.pop());
System.out.println("After pop, top element: " + stack.peek());
}
}