JavaRush/Courses/Kotlin SELF/ArrayDeque: queue and stack out of the box, basic operati...

ArrayDeque: queue and stack out of the box, basic operations, and pitfalls

Open

1. Why another structure if we already have MutableList?

When you’ve just gotten comfortable with MutableList, it can feel like you found a universal Swiss Army knife. You want to store everything as a list: command history, a task queue, an action stack. And yes, you can—Kotlin doesn’t forbid it. But sometimes a list starts behaving like luggage without a handle: you can carry it, but it’s constantly inconvenient.

The problem usually shows up in two situations. First, you need to quickly add/take elements from the front (for example, “first in—first out”). Second, you need to quickly work with the latest elements, like in an action history (“last action—undo it”). In these cases, ArrayDeque is often easier to read and more honest in terms of intent.

From the Kotlin standard library perspective, ArrayDeque<T> is a double-ended queue (deque): you can add and remove elements from both the front and the back. It’s implemented on top of a “resizable array” that grows as needed.

2. ArrayDeque in plain terms: a double-ended queue

If you translate the term “deque” into everyday analogies, it’s something like “a queue with a door at both the front and the back.” A regular store line works strictly from two ends: you join at the back, you get served from the front. And ArrayDeque is a queue where the cashier can also serve from the other end (a weird store, but the data structure is happy).

What matters is that ArrayDeque in Kotlin covers two classic scenarios at once. If you use only the back for adding and the front for removing—that’s a FIFO queue. If you use only the back for both adding and removing—that’s a LIFO stack. And all of this is done with standard methods, without reinventing wheels on top of MutableList.

Below is a mini diagram to lock it in visually:

flowchart LR
    subgraph FIFO["FIFO queue"]
      A1["addLast(x)"] --> A2["... queue ..."] --> A3["removeFirst()"]
    end

    subgraph LIFO["LIFO stack"]
      B1["addLast(x)"] --> B2["... stack ..."] --> B3["removeLast()"]
    end

3. Basic operations of ArrayDeque

Before we “plug” ArrayDeque into our learning app, it’s important to master the most basic mechanics. With MutableList, we got used to add(...), removeAt(...), indices, and lastIndex. In ArrayDeque, the focus is different: we work with the front/back, not with indices.

You can memorize the main operations like this: addFirst/addLast means “put an element,” removeFirst/removeLast means “take an element and remove it,” and first()/last() means “peek at an element without removing it.” Kotlin’s docs show this directly with a small example.

Creating an ArrayDeque

import kotlin.collections.ArrayDeque

fun main() {
    val empty = ArrayDeque<Int>()
    println(empty) // []

    val deque = ArrayDeque(listOf(1, 2, 3))
    println(deque) // [1, 2, 3]
}

Notice a nice Kotlin collections detail: even if a collection is stored in a val, you can still mutate it (the contents change, not the reference). This is the general principle for mutable collections.

Adding and peeking the first/last

import kotlin.collections.ArrayDeque

fun main() {
    val d = ArrayDeque(listOf(1, 2, 3))

    d.addFirst(0)
    d.addLast(4)

    println(d)         // [0, 1, 2, 3, 4]
    println(d.first()) // 0
    println(d.last())  // 4
}

This snippet almost repeats the example from the official collections page, and that’s good: if an example is “canonical,” your brain has an easier time anchoring on it.

Removing and checking emptiness

Removal is where beginners most often “catch” an exception and then sadly stare at the stack trace. The point is that removeFirst() and removeLast() assume: the element exists. If the deque is empty, you’ll get an error.

So an important reflex: before removing, check isNotEmpty() or isEmpty(). These functions exist on Kotlin collections and are constantly useful in defensive code.

import kotlin.collections.ArrayDeque

fun main() {
    val d = ArrayDeque<Int>()

    if (d.isNotEmpty()) {
        println(d.removeFirst())
    } else {
        println("Deque is empty, nothing to remove") // Deque is empty, nothing to remove
    }
}

4. A FIFO queue with ArrayDeque

A FIFO queue is one of the most natural real-life patterns: tasks arrive, and we process them in order. In programming, queues show up in “task scheduler” logic, “message backlog to process,” “document printing,” and “batch command processing.” Even if you’re not building complex systems yet, a queue is a useful mental model.

With ArrayDeque, a FIFO queue is as simple as it gets: add to the back via addLast, take from the front via removeFirst. In other words, you’re telling the structure: “put the new one at the tail” and “serve the first one at the head.”

Here’s a short demo:

import kotlin.collections.ArrayDeque

fun main() {
    val q = ArrayDeque<String>()

    q.addLast("read file")
    q.addLast("parse data")
    q.addLast("build report")

    println(q.removeFirst()) // read file
    println(q.removeFirst()) // parse data
    println(q)               // [build report]
}

If you noticed, there isn’t a single index here. That’s not a “limitation,” it’s a “hint”: the structure makes your code closer to the intent. If you implement a queue through list indices, you’re already slightly pretending you need a list, when you actually need a queue.

5. A LIFO stack with ArrayDeque

A LIFO stack is another everyday logic: you stack plates, and you take the top one. In programming, a stack usually appears wherever there is “history” and “rollback”: user action history, command history, nested calls, backtracking. We won’t dive into theoretical weeds, but a practical undo scenario is clear even to a beginner.

With ArrayDeque, a stack is done like this: add to the back via addLast, and also take from the back via removeLast. From a code-reading perspective it looks like “push on top” and “pop from top.”

import kotlin.collections.ArrayDeque

fun main() {
    val stack = ArrayDeque<String>()

    stack.addLast("open settings")
    stack.addLast("change theme")
    stack.addLast("save")

    println(stack.removeLast()) // save
    println(stack.removeLast()) // change theme
    println(stack)              // [open settings]
}

Yes, you could do the same with MutableList and removeAt(lastIndex). But ArrayDeque wins here because the method names describe the intent: “remove from the end” is exactly what you are doing.

6. ArrayDeque in a CLI app: add-history and undo

Now the most interesting part: let’s carefully “land” ArrayDeque into our practical project (a console expense/notes tracker). In previous days we already made commands like add, list, remove, and learned how to update collections without breaking iteration. Today we’ll add a small but very real feature: undo the last add.

Upfront honesty: we’ll keep undo simple. It will roll back only the latest add operations (and only in the order they happened). This is not production Git; it’s a learning example—we want to feel why a “history stack” is an ArrayDeque.

Mini data model

Since classes and data class are still ahead for us, we’ll store expenses as Pair<String, Int>, where first is the category and second is the amount. We already know this from previous days about Pair/Triple.

typealias Expense = Pair<String, Int> // if your course hasn't covered typealias yet, just remove this line

If typealias hasn’t been covered in your track yet, no problem: you can write Pair<String, Int> directly in the code. The meaning doesn’t change.

Add-history stack

The idea is: when the user enters add, we append a record to the expenses list and in parallel push the same record into the history stack. When the user enters undo, we pop the last record from history and remove the last record from the expenses list.

import kotlin.collections.ArrayDeque

fun main() {
    val expenses = mutableListOf<Pair<String, Int>>()
    val history = ArrayDeque<Pair<String, Int>>() // stack: the latest add is on top

    expenses.add("food" to 250)
    history.addLast("food" to 250)

    expenses.add("taxi" to 600)
    history.addLast("taxi" to 600)

    println(expenses) // [(food, 250), (taxi, 600)]
}

Implementing undo

Now we’ll do a “naive but clear” undo: it works only if there is something to roll back. That’s why we use the isNotEmpty() check—it’s that small safety fuse that saves you from removing from an empty structure.

import kotlin.collections.ArrayDeque

fun undoLastAdd(
    expenses: MutableList<Pair<String, Int>>,
    history: ArrayDeque<Pair<String, Int>>
) {
    if (history.isEmpty()) {
        println("Nothing to undo") // Nothing to undo
        return
    }

    history.removeLast()
    expenses.removeLast()
    println("Last add has been undone") // Last add has been undone
}

There’s a subtle point here: removeLast() on MutableList removes the last element. We assume history and expenses are synchronized (we always added to them in pairs). That’s a reasonable learning model. In a real project you’d defend this more, but for now our goal is to understand the structure.

Mini command loop: add and undo

Let’s assemble a small “command loop” snippet. It doesn’t claim to be perfect, but it shows where ArrayDeque fits conceptually.

import kotlin.collections.ArrayDeque

fun main() {
    val expenses = mutableListOf<Pair<String, Int>>()
    val history = ArrayDeque<Pair<String, Int>>()

    while (true) {
        val line = readln().trim()
        if (line == "exit") break

        val parts = line.split(" ")
        when (parts[0]) {
            "add" -> {
                val cat = parts.getOrNull(1) ?: continue
                val amount = parts.getOrNull(2)?.toIntOrNull() ?: continue

                val e = cat to amount
                expenses.add(e)
                history.addLast(e)

                println("OK") // OK
            }
            "undo" -> {
                if (history.isNotEmpty()) {
                    history.removeLast()
                    expenses.removeLast()
                    println("UNDO") // UNDO
                } else {
                    println("Nothing to undo") // Nothing to undo
                }
            }
            "list" -> println(expenses)
            else -> println("Unknown command")
        }
    }
}

Yes, there are places you could improve (input validation, error messages, supporting remove, and history reconciliation). But that’s fine: we added ArrayDeque as a tool without turning the example into a “console mega-machine.” In learning code, it’s useful to stop in time—otherwise it becomes hard to see which part is responsible for what.

7. When ArrayDeque is not a good fit

It’s tempting to fall in love with a new structure and start stuffing it everywhere, but it’s better to set emotions aside and switch to engineering mode. ArrayDeque is great when you really work with the ends of the collection. If you need random access (“give me the element at index i”), if you need to sort constantly, if you need key-based lookup—that’s already the territory of List/Map and the collection operations we covered earlier.

It’s also important to remember that ArrayDeque is a mutable structure. It’s excellent for “accumulate and consume” scenarios, but if you build pipelines in a “don’t mutate the source data” style (like in our discussions about Sequence and readable chains), then ArrayDeque more often acts as a “working container” rather than as a “result of computation.”

If you want a simple “mental filter,” keep a table like this in mind:

What you want to do most often Candidate tool Why
Add/remove from the front and back
ArrayDeque
Methods directly express intent (addFirst/removeLast, etc.)
Work by indices, keep order, sort
List / MutableList
This is the “home base” for indices and sorting
Key-based access and accumulating “key → value”
Map / MutableMap
Lookup by key is the map’s main contract
Build reports and totals without mutation
collection ops + fold
Pure computations are easier to read

Where else ArrayDeque often appears

To broaden your horizons a bit (without any obligation to “go grind programming contests”), it’s useful to know: queues and deques often appear in graph traversals and pathfinding algorithms. In Kotlin materials this even shows up in Advent of Code writeups, where ArrayDeque is mentioned as a convenient queue.

This doesn’t mean you urgently need to learn graphs. It means that today you’re already picking up a tool that’s useful both in “everyday application development” (action history, task processing) and in more algorithmic problems, if you get to them.

8. Typical mistakes when working with ArrayDeque

Mistake No. 1: removing from an empty deque without checking.
removeFirst() and removeLast() assume the element exists. If the deque is empty, you’ll get an exception. The fix is simple: before removing, check isNotEmpty() or isEmpty(). This pattern is universal for Kotlin collections and regularly saves you from “crashing out of nowhere.”

Mistake No. 2: mixing up queue and stack in the same structure.
A very common logical confusion: you add via addLast and also remove via removeLast, thinking you made FIFO, when you actually made LIFO. A simple self-check helps: FIFO is always “add to tail—take from head,” i.e., addLast + removeFirst. LIFO is always “push on top—pop from top,” i.e., addLast + removeLast.

Mistake No. 3: trying to use ArrayDeque like a list with indices.
When your hand reaches for something like “give me deque[3],” that’s a signal: you chose the wrong structure. ArrayDeque is about the ends, not about random access. If you need index-based access—go back to List/MutableList; the code will be more honest and easier to read.

Mistake No. 4: keeping history in ArrayDeque but not keeping it in sync with the main data.
In the undo example we kept expenses and history synchronized. If in real code you sometimes add an expense to expenses but forget to add it to history, then undo will start rolling back “the wrong thing.” This is not an ArrayDeque bug—it’s a data discipline bug. The fix is to apply changes to the main state and to the history side-by-side in the same code path, as one atomic action.

Mistake No. 5: turning ArrayDeque into “just another collection, just in case.”
Sometimes people add ArrayDeque “for the future” even though the program uses neither a queue nor a stack. The result is extra state you have to maintain. A good criterion: if you can’t explain in one sentence where the structure’s “front” and “back” are in your logic, you probably don’t need it yet.

1
Task
Kotlin SELF,  25 level,  4 lecture
Locked
Double-ended queue
Double-ended queue
1
Task
Kotlin SELF,  25 level,  4 lecture
Locked
Support queue
Support queue
1
Task
Kotlin SELF,  25 level,  4 lecture
Locked
Parenthesis Checker
Parenthesis Checker
1
Task
Kotlin SELF,  25 level,  4 lecture
Locked
Notes stack
Notes stack
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet