Skip to content

Iterator

Also known as

  • Cursor

Intent

Provide a way to access elements of an aggregate object sequentially without exposing its underlying representation.

Explanation

Real-world example

Imagine visiting a library with a vast collection of books organized in different sections such as fiction, non-fiction, science, etc. Instead of searching through every shelf yourself, the librarian provides you with a specific guidebook or a digital catalog for each section. This guidebook acts as an "iterator," allowing you to go through the books section by section, or even skip to specific types of books, without needing to know how the books are organized on the shelves.

In plain words

The Iterator pattern provides a method to sequentially access elements of a collection without exposing its underlying structure.

Wikipedia says

In object-oriented programming, the iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container's elements.

Sequence diagram

sequenceDiagram
    participant Client
    participant TreasureChest
    participant TreasureChestItemIterator

    Client ->> TreasureChest: iterator(itemType)
    TreasureChest -->> Client: Iterator~Item~
    loop hasNext()
        Client ->> TreasureChestItemIterator: hasNext()
        TreasureChestItemIterator -->> Client: true
        Client ->> TreasureChestItemIterator: next()
        TreasureChestItemIterator -->> Client: Item
    end
    Client ->> TreasureChestItemIterator: hasNext()
    TreasureChestItemIterator -->> Client: false

Programmatic Example

The main class in our example is the TreasureChest that contains items.

data class Item(val type: ItemType, val name: String) {
    override fun toString() = name
}

enum class ItemType {
    ANY,
    POTION,
    RING,
    WEAPON,
    ;

    override fun toString() = name.lowercase()
}

The TreasureChest class stores items and returns a filtered iterator.

class TreasureChest {
    private val items: List<Item> =
        listOf(
            Item(ItemType.POTION, "Potion of courage"),
            Item(ItemType.RING, "Ring of shadows"),
            Item(ItemType.POTION, "Potion of wisdom"),
            Item(ItemType.POTION, "Potion of blood"),
            Item(ItemType.WEAPON, "Sword of silver +1"),
            Item(ItemType.POTION, "Potion of rust"),
            Item(ItemType.POTION, "Potion of healing"),
            Item(ItemType.RING, "Ring of armor"),
            Item(ItemType.WEAPON, "Steel halberd"),
            Item(ItemType.WEAPON, "Dagger of poison"),
        )

    fun iterator(itemType: ItemType): Iterator<Item> =
        TreasureChestItemIterator(items, itemType)

    fun getItems(): List<Item> = items.toList()
}

The TreasureChestItemIterator implements Kotlin's built-in Iterator interface, filtering items by type.

internal class TreasureChestItemIterator(
    private val items: List<Item>,
    private val type: ItemType,
) : Iterator<Item> {
    private var idx = -1

    override fun hasNext(): Boolean = findNextIdx() != -1

    override fun next(): Item {
        idx = findNextIdx()
        check(idx != -1) { "No more items" }
        return items[idx]
    }

    private fun findNextIdx(): Int {
        var tempIdx = idx
        while (true) {
            tempIdx++
            if (tempIdx >= items.size) return -1
            if (type == ItemType.ANY ||
                items[tempIdx].type == type
            ) {
                return tempIdx
            }
        }
    }
}

A second example demonstrates iteration over a binary search tree. The TreeNode class represents a BST node.

class TreeNode<T : Comparable<T>>(val value: T) {
    var left: TreeNode<T>? = null
        private set
    var right: TreeNode<T>? = null
        private set

    fun insert(newValue: T) {
        val parent = parentNodeFor(newValue)
        parent.insertChild(newValue)
    }

    override fun toString(): String = value.toString()
}

The BstIterator performs an in-order traversal using a stack, achieving O(h) extra space.

class BstIterator<T : Comparable<T>>(
    root: TreeNode<T>?,
) : Iterator<TreeNode<T>> {
    private val pathStack = ArrayDeque<TreeNode<T>>()

    init {
        pushPathToNextSmallest(root)
    }

    override fun hasNext(): Boolean =
        pathStack.isNotEmpty()

    override fun next(): TreeNode<T> {
        if (pathStack.isEmpty()) {
            throw NoSuchElementException()
        }
        val next = pathStack.pop()
        pushPathToNextSmallest(next.right)
        return next
    }
}

Here is the full example in action.

val treasureChest = TreasureChest()

logger.info("Item Iterator for ItemType ring: ")
val ringIterator = treasureChest.iterator(ItemType.RING)
ringIterator.forEach { logger.info(it.toString()) }

logger.info("BST Iterator: ")
val root = TreeNode(8).apply {
    insert(3)
    insert(10)
    insert(1)
    insert(6)
    insert(14)
    insert(4)
    insert(7)
    insert(13)
}
val bstIterator = BstIterator(root)
bstIterator.forEach { logger.info("Next node: ${it.value}") }

Program output:

Item Iterator for ItemType ring:
Ring of shadows
Ring of armor
BST Iterator:
Next node: 1
Next node: 3
Next node: 4
Next node: 6
Next node: 7
Next node: 8
Next node: 10
Next node: 13
Next node: 14

Class diagram

classDiagram
    class Iterator~T~ {
        <<interface>>
        +hasNext() Boolean
        +next() T
    }
    class ItemType {
        <<enumeration>>
        ANY
        POTION
        RING
        WEAPON
        +toString() String
    }
    class Item {
        +type ItemType
        +name String
        +toString() String
    }
    class TreasureChest {
        -items List~Item~
        +iterator(ItemType) Iterator~Item~
        +getItems() List~Item~
    }
    class TreasureChestItemIterator {
        -items List~Item~
        -type ItemType
        -idx Int
        +hasNext() Boolean
        +next() Item
    }
    class TreeNode~T~ {
        +value T
        +left TreeNode~T~
        +right TreeNode~T~
        +insert(T)
        +toString() String
    }
    class BstIterator~T~ {
        -pathStack ArrayDeque~TreeNode~
        +hasNext() Boolean
        +next() TreeNode~T~
    }

    TreasureChestItemIterator ..|> Iterator~Item~
    BstIterator ..|> Iterator~TreeNode~
    TreasureChest --> TreasureChestItemIterator : creates
    TreasureChest o-- Item : contains
    Item --> ItemType
    BstIterator --> TreeNode : traverses

Applicability

Use the Iterator pattern when:

  • You need to access an aggregate object's contents without exposing its internal representation.
  • You want to support multiple traversals of aggregate objects.
  • You want to provide a uniform interface for traversing different aggregate structures.

Consequences

Benefits:

  • Reduces the coupling between data structures and algorithms used for iteration.
  • Provides a uniform interface for iterating over various types of data structures, enhancing code reusability and flexibility.

Trade-offs:

  • Overhead of using an iterator object may slightly reduce performance compared to direct traversal.
  • Complex aggregate structures may require complex iterators that can be difficult to manage or extend.

Credits