Skip to Content
Course content

243: Practice Exercise: Implementing a Custom Collection Type

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why bother building a custom collection instead of just wrapping a List?

I get this a lot. On the surface, it seems easier to just create a class that holds a ArrayList and write a few helper methods to manage it. But that's a leaky abstraction. If you're building something like a LimitedSizeList—a list that automatically kicks out the oldest element when it hits a capacity limit—you don't want the rest of your codebase to have to remember to call trim() every time they add an item.

By implementing the actual collection interfaces, you're telling the rest of the Kotlin ecosystem, "I am a collection." This means your custom type can be passed into any function that expects a Collection or Iterable, and it works seamlessly with filter, map, and forEach without you having to write those operators yourself. It's about encapsulation; the "rules" of your collection are baked into the type, not scattered across your business logic.

Which interface should I actually implement?

This is where people usually over-engineer things. Don't feel like you have to implement MutableCollection right out of the gate—it has a ton of methods that you might not actually need, and implementing them all can be a slog.

Start with Iterable<T>. If you only need to loop over your data, that's all you need. If you need to provide a size or check if an item contains another, move up to Collection<T>. If you need to add and remove elements, then you go for MutableCollection<T>. Here is a skeleton of how I usually approach a limited-capacity list:

class LimitedSizeList<T>(private val limit: Int) : MutableCollection<T> {
    private val internalList = mutableListOf<T>()

    override fun add(element: T): Boolean {
        if (internalList.size >= limit) {
            internalList.removeAt(0) // Kick out the oldest
        }
        return internalList.add(element)
    }

    override val size: Int get() = internalList.size
    override fun isEmpty(): Boolean = internalList.isEmpty()
    override fun contains(element: T): Boolean = internalList.contains(element)
    override fun containsAll(elements: Collection<T>): Boolean = internalList.containsAll(elements)
    override fun remove(element: T): Boolean = internalList.remove(element)
    override fun removeAll(elements: Collection<T>): Boolean = internalList.removeAll(elements)
    override fun retainAll(elements: Collection<T>): Boolean = internalList.retainAll(elements)
    override fun clear() = internalList.clear()
    
    override fun iterator(): MutableIterator<T> = internalList.iterator()
}

How do I make it feel like a "real" Kotlin collection?

If you stop at the interfaces, your collection is functional, but it doesn't "feel" like Kotlin. You'll be calling myList.get(0) or myList.add(item). To make it feel native, you want to use operator overloading. Specifically, the get and set operators.

Once you add these, you can use the square bracket syntax [], which is what every Kotlin developer expects. I usually add these as extension-like members within the class:

// Inside LimitedSizeList class
operator fun get(index: Int): T = internalList[index]

operator fun set(index: Int, element: T) {
    internalList[index] = element
}

Now, instead of some clunky method call, you can just write val item = myLimitedList[2]. It's a small touch, but it makes your custom type feel like a first-class citizen of the language.

What's the deal with the iterator?

The iterator() method is the secret sauce. It's what allows your custom type to work in a for (item in collection) loop. In the example above, I just delegated the call to the internalList.iterator(). This is almost always the right move if your custom collection is wrapping another standard collection.

If you were building a collection from scratch—say, a linked list using custom Node objects—you'd need to implement the Iterator interface yourself, overriding hasNext() and next(). But unless you're doing a computer science homework assignment, you're probably better off delegating to a backing store like an ArrayList or ArrayDeque to avoid reinventing the wheel and introducing bugs into your pointer logic.




📋 Practical Task

Exercise: Build a SlidingWindowBuffer

Your task is to implement a custom collection called SlidingWindowBuffer. This collection should act as a fixed-size window that only keeps the most recent N elements added to it. If the buffer is full and a new element is added, the oldest element should be removed automatically.

  • Implement the MutableCollection<T> interface.
  • The constructor should take an capacity: Int.
  • Ensure that the add() method maintains the capacity limit (FIFO - First In, First Out).
  • Implement the get operator so that you can access elements by index using buffer[index].
  • Ensure the iterator() is correctly implemented so the buffer can be used in a for loop.

Test your implementation by: Creating a buffer with a capacity of 3, adding the numbers 1, 2, 3, 4, and 5, and then printing the buffer to verify that it only contains [3, 4, 5].

Rating
0 0

There are no comments for now.

to be the first to leave a comment.