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.
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:
-
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.
-
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.
-
Interview Preparation: Many coding interviews test your ability to design structures like stacks or queues from scratch.
-
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.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.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
@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
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
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.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.


