Implementing Custom Data Structures in Java: Building Your Own Stack, Queue, and Map

In this article, we’ll explore how to design and code these data structures in Java manually, discuss their core principles, and understand where custom implementations are useful.

Implementing Custom Data Structures in Java: Building Your Own Stack, Queue, and Map

When most developers think of data structures in Java, the first things that come to mind are the ready-made classes in the Java Collections Framework—ArrayList, HashMap, Stack, and Queue. These are powerful, efficient, and easy to use. Yet, building your own custom data structures is one of the best ways to truly understand how they work under the hood.

By implementing core structures like a Stack, Queue, and Map from scratch, you gain insight into how Java manages memory, references, and algorithmic efficiency. 

Why Build Custom Data Structures?

Before diving into code, it’s worth asking: why bother writing your own when Java already provides them?

The answer lies in learning and optimization:

  1. Deep Understanding: Writing your own implementation clarifies how data structures store and retrieve information, how pointers (references) work, and what trade-offs exist between speed and memory.

  2. Customization: Sometimes built-in data structures in Java don’t fully match an application’s needs—for example, implementing a bounded queue or a time-decaying cache.

  3. Interview Preparation: Many coding interviews test your ability to design structures like stacks or queues from scratch.

  4. Performance Tuning: Custom data structures can be fine-tuned for specific workloads, e.g., constant-time access patterns or fixed memory environments.

Let’s start with the simplest structure and build our way up.

1. Implementing a Stack in Java

Concept

A Stack is a LIFO (Last-In-First-Out) data structure. The last element pushed is the first one popped. Think of a stack of plates—add from the top, remove from the top.

Core Operations

  • push(item): Add an item to the top.

  • pop(): Remove and return the top item.

  • peek(): Return the top item without removing it.

  • isEmpty(): Check if the stack is empty.

Implementation Using Arrays

public class CustomStack {

    private T[] elements;

    private int top;

    private int capacity;

 

    @SuppressWarnings("unchecked")

    public CustomStack(int size) {

        elements = (T[]) new Object[size];

        capacity = size;

        top = -1;

    }

 

    public void push(T item) {

        if (top == capacity - 1)

            throw new RuntimeException("Stack Overflow");

        elements[++top] = item;

    }

 

    public T pop() {

        if (isEmpty())

            throw new RuntimeException("Stack Underflow");

        return elements[top--];

    }

 

    public T peek() {

        if (isEmpty())

            throw new RuntimeException("Stack is empty");

        return elements[top];

    }

 

    public boolean isEmpty() {

        return top == -1;

    }

}

 

Example Use

CustomStack stack = new CustomStack<>(5);

stack.push(10);

stack.push(20);

System.out.println(stack.pop()); // 20

 

How It Works

This simple implementation uses an array and a top pointer to track the current position. It demonstrates how

 can be controlled manually, giving insight into memory management and indexing.

2. Implementing a Queue in Java

Concept

A Queue follows the FIFO (First-In-First-Out) principle. Think of a line at a ticket counter—the first to arrive is the first served.

Core Operations

  • enqueue(item): Add an item to the rear.

  • dequeue(): Remove and return the front item.

  • peek(): View the front item without removing it.

  • isEmpty(): Check if the queue is empty.

Implementation Using a Circular Array

public class CustomQueue {

    private T[] elements;

    private int front, rear, size, capacity;

 

    @SuppressWarnings("unchecked")

    public CustomQueue(int capacity) {

        this.capacity = capacity;

        elements = (T[]) new Object[capacity];

        front = 0;

        rear = -1;

        size = 0;

    }

 

    public void enqueue(T item) {

        if (size == capacity)

            throw new RuntimeException("Queue is full");

        rear = (rear + 1) % capacity;

        elements[rear] = item;

        size++;

    }

 

    public T dequeue() {

        if (isEmpty())

            throw new RuntimeException("Queue is empty");

        T item = elements[front];

        front = (front + 1) % capacity;

        size--;

        return item;

    }

 

    public T peek() {

        if (isEmpty())

            throw new RuntimeException("Queue is empty");

        return elements[front];

    }

 

    public boolean isEmpty() {

        return size == 0;

    }

}

 

Example Use

CustomQueue queue = new CustomQueue<>(3);

queue.enqueue("A");

queue.enqueue("B");

System.out.println(queue.dequeue()); // "A"

How It Works

This implementation uses modular arithmetic to wrap around the array (making it circular). It prevents wasted space after dequeuing. In real-world data structures in Java, circular queues are used for buffering tasks—like in I/O processing, printers, and network packet management.

3. Implementing a Simple Map in Java

Concept

A Map stores key–value pairs and allows quick lookups based on the key. In Java, the HashMap is the standard implementation, but we can build a simplified version to understand its internal logic.

Core Operations

  • put(key, value): Store a key–value pair.

  • get(key): Retrieve the value by key.

  • remove(key): Delete a key–value pair.

  • containsKey(key): Check if a key exists.

Implementation Using Hashing

import java.util.LinkedList;

 

class Entry {

    K key;

    V value;

    Entry(K key, V value) {

        this.key = key;

        this.value = value;

    }

}

 

public class CustomHashMap {

    private final int SIZE = 10;

    private LinkedList>[] table;

 

    @SuppressWarnings("unchecked")

    public CustomHashMap() {

        table = new LinkedList[SIZE];

        for (int i = 0; i < SIZE; i++) {

            table[i] = new LinkedList<>();

        }

    }

 

    private int hash(K key) {

        return Math.abs(key.hashCode() % SIZE);

    }

 

    public void put(K key, V value) {

        int index = hash(key);

        for (Entry entry : table[index]) {

            if (entry.key.equals(key)) {

                entry.value = value;

                return;

            }

        }

        table[index].add(new Entry<>(key, value));

    }

 

    public V get(K key) {

        int index = hash(key);

        for (Entry entry : table[index]) {

            if (entry.key.equals(key)) {

                return entry.value;

            }

        }

        return null;

    }

 

    public void remove(K key) {

        int index = hash(key);

        table[index].removeIf(entry -> entry.key.equals(key));

    }

 

    public boolean containsKey(K key) {

        return get(key) != null;

    }

}

Example Use

CustomHashMap map = new CustomHashMap<>();

map.put("Alice", 85);

map.put("Bob", 92);

System.out.println(map.get("Bob")); // 92

How It Works

This implementation uses hashing to compute an index from the key’s hash code. Collisions are handled using chaining with linked lists. This mirrors the internal mechanism of Java’s built-in HashMap, which uses an array of buckets and linked nodes.

Understanding how this works gives developers deeper insight into performance tuning—like load factors, rehashing, and O(1) average access time.

Comparing the Three Data Structures

Structure

Principle

Time Complexity (Average)

Example Use Case

Stack

LIFO

Push/Pop: O(1)

Undo feature, expression evaluation

Queue

FIFO

Enqueue/Dequeue: O(1)

Task scheduling, message queues

Map

Key–Value

Insert/Lookup: O(1)

Caching, lookups, indexing

Each of these data structures in Java addresses specific types of problems and access patterns. Knowing which to use—and why—can greatly improve the performance and clarity of your code.

When to Build vs When to Use Built-In Structures

Scenario

Use Built-In

Build Custom

General development

HashMap, ArrayDeque, Stack

❌ Not necessary

Learning & academic projects

✅/❌

✅ Highly recommended

Performance tuning for special workloads

✅ Custom logic can help

Constrained environments (IoT, embedded)

✅ To reduce dependencies

In professional environments, developers usually rely on the Java Collections Framework because it’s well-tested and optimized. However, knowing how to implement core structures manually helps when debugging or optimizing large systems.

The Role of Data Structures in Java Applications

Beyond simple algorithms, data structures in Java are at the heart of enterprise applications, AI systems, and databases:

  • Stacks manage recursion and expression parsing in compilers.

  • Queues support asynchronous task handling in frameworks like Spring Boot.

  • HashMaps are the foundation for caching, configurations, and object mapping in frameworks like Hibernate.

Even high-level technologies like machine learning libraries, distributed systems, and big-data tools rely heavily on efficient Java data structure design.

Conclusion

Implementing custom data structures in Java is one of the most effective ways to move from being a code user to a code designer. By building your own Stack, Queue, and Map, you not only learn their operations and complexities but also understand how Java optimizes memory, references, and performance behind the scenes.

While production code typically uses Java’s built-in structures, your own implementations serve as valuable learning tools and can be tailored for specialized applications. In a world where efficiency and scalability matter more than ever, mastering these core data structures—and knowing how to craft them yourself—remains a critical skill for every Java developer.