-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
243: Practice Exercise: Implementing a Custom Collection Type
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 afor (item in collection)loop. In the example above, I just delegated the call to theinternalList.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
Nodeobjects—you'd need to implement theIteratorinterface yourself, overridinghasNext()andnext(). But unless you're doing a computer science homework assignment, you're probably better off delegating to a backing store like anArrayListorArrayDequeto 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
getoperator so that you can access elements by index usingbuffer[index]. - Ensure the
iterator()is correctly implemented so the buffer can be used in aforloop.
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].
There are no comments for now.