Stack Class in Java: Implementation & Examples


7 min read 07-11-2024
Stack Class in Java: Implementation & Examples

Java's Stack class is a fundamental data structure that plays a crucial role in various programming scenarios. It embodies the Last-In, First-Out (LIFO) principle, meaning the last element added to the stack is the first one to be removed. This principle, akin to a pile of dishes where the last plate added is the first one you grab, makes stacks particularly suitable for tasks like function call management, expression evaluation, and undo/redo functionalities.

Understanding the LIFO Principle

Imagine a stack of books on a table. To retrieve the bottom book, you must remove all the books above it. The book you placed last is the first one you can access. The same logic applies to a Stack in Java. Each element is pushed onto the top of the stack, and retrieval happens in the reverse order.

Methods of the Stack Class

The Stack class in Java provides a set of methods designed to manipulate stack elements efficiently. Let's delve into some of the most commonly used ones:

1. push(E item): This method adds an element to the top of the stack. It takes an object as input and pushes it onto the stack. Think of this as placing a new book on top of the existing pile.

2. pop(): This method removes and returns the element at the top of the stack. It's like taking the top book off the pile. If the stack is empty, it throws an EmptyStackException.

3. peek(): This method returns the element at the top of the stack without removing it. It's akin to looking at the top book without actually removing it. If the stack is empty, it throws an EmptyStackException.

4. empty(): This method checks if the stack is empty and returns true if it is, false otherwise. It helps you avoid potential errors when attempting to access elements on an empty stack.

5. search(Object o): This method searches for a specific object in the stack and returns the distance from the top of the stack. If the object is found, it returns its position (counting from the top, with 1 being the top element); otherwise, it returns -1.

6. size(): This method returns the number of elements currently present in the stack. It tells you how many books are in your pile.

7. clear(): This method removes all the elements from the stack. It clears the entire pile of books, leaving the stack empty.

Implementation & Examples

Let's see the Stack class in action with some practical examples:

Example 1: Basic Stack Operations

import java.util.Stack;

public class StackExample {
    public static void main(String[] args) {
        Stack<String> bookStack = new Stack<>(); // Creating a stack of strings

        // Adding books to the stack
        bookStack.push("Java for Beginners");
        bookStack.push("Algorithms & Data Structures");
        bookStack.push("Introduction to Programming");

        // Printing the stack contents
        System.out.println("Books in the stack: " + bookStack);

        // Retrieving and removing the top book
        String topBook = bookStack.pop();
        System.out.println("Top book: " + topBook);

        // Checking if the stack is empty
        System.out.println("Is the stack empty? " + bookStack.empty());

        // Retrieving the top book without removing it
        String peekBook = bookStack.peek();
        System.out.println("Top book (without removing): " + peekBook);

        // Removing all books from the stack
        bookStack.clear();
        System.out.println("Stack after clearing: " + bookStack);
    }
}

Output:

Books in the stack: [Introduction to Programming, Algorithms & Data Structures, Java for Beginners]
Top book: Introduction to Programming
Is the stack empty? false
Top book (without removing): Algorithms & Data Structures
Stack after clearing: []

Example 2: Expression Evaluation

Let's consider the task of evaluating an arithmetic expression using a stack. For instance, how would you evaluate the expression "2 + 3 * 4"?

import java.util.Stack;

public class ExpressionEvaluator {
    public static void main(String[] args) {
        String expression = "2 + 3 * 4";

        // Splitting the expression into tokens (numbers and operators)
        String[] tokens = expression.split(" ");

        // Creating two stacks: one for numbers, another for operators
        Stack<Integer> numberStack = new Stack<>();
        Stack<Character> operatorStack = new Stack<>();

        // Evaluating the expression
        for (String token : tokens) {
            if (token.matches("\\d+")) { // If it's a number
                numberStack.push(Integer.parseInt(token));
            } else { // If it's an operator
                operatorStack.push(token.charAt(0));
            }

            // Processing operators
            if (!operatorStack.isEmpty() && !numberStack.isEmpty() && isHigherPrecedence(operatorStack.peek(), token.charAt(0))) {
                int operand2 = numberStack.pop();
                int operand1 = numberStack.pop();
                char operator = operatorStack.pop();

                int result = calculate(operand1, operand2, operator);
                numberStack.push(result);
            }
        }

        // Calculating the final result
        while (!operatorStack.isEmpty()) {
            int operand2 = numberStack.pop();
            int operand1 = numberStack.pop();
            char operator = operatorStack.pop();

            int result = calculate(operand1, operand2, operator);
            numberStack.push(result);
        }

        System.out.println("Result: " + numberStack.pop());
    }

    // Helper functions for operator precedence and calculation
    private static boolean isHigherPrecedence(char operator1, char operator2) {
        if (operator1 == '*' || operator1 == '/') {
            return true;
        }
        return false;
    }

    private static int calculate(int operand1, int operand2, char operator) {
        switch (operator) {
            case '+': return operand1 + operand2;
            case '-': return operand1 - operand2;
            case '*': return operand1 * operand2;
            case '/': return operand1 / operand2;
            default: return 0;
        }
    }
}

Output:

Result: 14

Example 3: Undo/Redo Functionality

Imagine you're editing a text document. You might want to undo your last change or redo a previously undone change. This is where the Stack class comes in handy.

import java.util.Stack;

public class TextEditor {
    private String currentText;
    private Stack<String> undoStack = new Stack<>();
    private Stack<String> redoStack = new Stack<>();

    public TextEditor(String initialText) {
        this.currentText = initialText;
    }

    public void appendText(String text) {
        undoStack.push(currentText);
        currentText += text;
        redoStack.clear(); // Clearing redo stack when a new change is made
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            redoStack.push(currentText);
            currentText = undoStack.pop();
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            undoStack.push(currentText);
            currentText = redoStack.pop();
        }
    }

    public String getCurrentText() {
        return currentText;
    }

    public static void main(String[] args) {
        TextEditor editor = new TextEditor("Hello, ");
        System.out.println("Initial text: " + editor.getCurrentText());

        editor.appendText("world!");
        System.out.println("Text after appending: " + editor.getCurrentText());

        editor.undo();
        System.out.println("Text after undo: " + editor.getCurrentText());

        editor.redo();
        System.out.println("Text after redo: " + editor.getCurrentText());
    }
}

Output:

Initial text: Hello, 
Text after appending: Hello, world!
Text after undo: Hello, 
Text after redo: Hello, world!

Advantages of Using the Stack Class

  • Simplicity: The Stack class provides a straightforward and intuitive interface for manipulating data, making it easy to understand and use.
  • Efficiency: The LIFO structure allows for efficient insertion and removal of elements, especially when dealing with recent data.
  • Functionality: The built-in methods like push, pop, peek, and empty simplify common stack operations.
  • Wide Applicability: Stacks find use in various programming domains, including function calls, expression evaluation, undo/redo systems, and more.

Considerations

  • Fixed Size: Unlike some implementations, the Java Stack class doesn't have a fixed size limit. However, keep in mind that pushing elements beyond the available memory can lead to OutOfMemoryError.
  • Thread Safety: The Java Stack class is not thread-safe. If you need to use a stack in a multithreaded environment, consider using a synchronized stack implementation or using a concurrent data structure like ConcurrentLinkedDeque to achieve thread safety.

FAQs

1. What are the key differences between a stack and a queue?

Answer: A stack follows the LIFO principle (Last-In, First-Out), while a queue adheres to FIFO (First-In, First-Out). Imagine a stack like a pile of books—you remove the top book first. In a queue, it's like a line at a store; the person who arrived first is served first.

2. Can I implement my own stack data structure in Java?

Answer: Absolutely! You can implement a stack using an array or a linked list. Implementing a stack from scratch allows for greater control and customization. You can find numerous examples online and within Java documentation.

3. What are some real-world applications of stacks?

Answer: Stacks are used extensively in various applications, including:

  • Function Call Management: When a function is called, its arguments and local variables are pushed onto a stack. When the function returns, these elements are popped from the stack.
  • Expression Evaluation: Stacks are crucial in converting infix expressions (like "2 + 3 * 4") to postfix (RPN) and evaluating postfix expressions.
  • Undo/Redo Functionality: Text editors and other applications use stacks to track user actions, allowing them to undo or redo changes.
  • Backtracking Algorithms: Algorithms like maze solving and Sudoku solvers often employ stacks to keep track of possible paths or states.

4. Is the Stack class in Java thread-safe?

Answer: No, the Stack class is not inherently thread-safe. If multiple threads access the same Stack object concurrently, data corruption may occur. You can address this by using a synchronized stack implementation or using a concurrent data structure like ConcurrentLinkedDeque.

5. How can I create a stack with a fixed size limit in Java?

Answer: The built-in Stack class doesn't have a fixed size limit. To implement a fixed-size stack, you can use an array with a predetermined size. You'll need to handle cases when the array is full and prevent further pushes. You can also explore libraries like Guava or Apache Commons Collections, which offer more sophisticated stack implementations.

Conclusion

The Stack class in Java offers a powerful and versatile tool for managing data in a Last-In, First-Out fashion. Its intuitive methods and wide applicability make it a valuable asset for developers working on a range of programming tasks. Understanding the LIFO principle and the key methods of the Stack class will enable you to leverage its potential in your own Java applications. Whether it's managing function calls, evaluating expressions, or implementing undo/redo functionality, the Stack class proves to be a crucial building block in the world of Java programming.