Java iterators are dead. Long live Kotlin iterators!

agafox

Oleg Agafonov

Posted on November 17, 2020

Java iterators are dead. Long live Kotlin iterators!

Iterator is a great pattern. It's extremely simple and yet very powerful. Long time ago I saw iterators literally everywhere but, when Java 8 came with streams API, things have changed. It doesn't mean that iterators disappeared fully. No, they just hid deep into libraries and frameworks you use every day. But few of them are still the most important players in modern and widely used Java SDKs.

SIP3 uses MongoDB Java Driver to read insanely big amounts of data. And as you know from my previous blog post we prefer to work with MongoCursor<Document> explicitly. Guess what? MongoCursor<Document> is nothing else but iterator šŸ˜Š.

Having MongoCursor<Document> as iterator totally makes sense though. Iterator's contract forces you to call hasNext() method every time when you want to fetch a new document from MongoDB collection. So, MongoDB Java Driver just makes you read not all documents but as many as you need. This approach saves a lot of resources on data transferring cause data now can be stored in MongoDB server and pulled from there on demand in multiple batches.

In the previous blog post I showed how we work with multiple MongoDB collections partitioned by time the same way you will work with just a single collection. However, it wasn't enough to aggregate the data from all the partitions we have. That's why, inspired by streams API, we decided to introduce a few extension functions.

āš ļø WARNING: Examples below are not perfect and didn't mean to be though. So, keep in mind that calling map() and merge() methods will change a calling iterator state too.

map()

No doubts map() is the most popular function of streams API. Of course it can be a great addition to our iterator:

fun <T, R> Iterator<T>.map(transform: (T) -> R): Iterator<R> {
    val i = this

    return object : Iterator<R> {

        override fun hasNext(): Boolean {
            return i.hasNext()
        }

        override fun next(): R {
            if (!hasNext()) throw NoSuchElementException()
            return transform.invoke(i.next())
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's see how we can call it:

// Int: 1, 2, 3
val numbers = listOf(1, 2, 3).iterator()

// String: "1", "2", "3"
val strings = numbers.map(Any::toString) 
Enter fullscreen mode Exit fullscreen mode

The cherry on the top šŸ’ is that transform function will be called lazily.

merge()

Not like map() this method is not a part of streams API. However, it let's us gather data from logically partitioned in multiple collections (you can read more about it in the previous blog post). Implementation is a bit complicated.

First we need to write a helper method:

fun <T> Iterator<T>.nextOrNull(): T? {
    return if (hasNext()) next() else null
}
Enter fullscreen mode Exit fullscreen mode

,and then our method extension itself:

fun <T> Iterator<T>.merge(o: Iterator<T>, comparator: Comparator<T>? = null): Iterator<T> {
    val i = this

    return object : Iterator<T> {

        var vi: T? = i.nextOrNull()
        var vo: T? = o.nextOrNull()

        override fun hasNext(): Boolean {
            return vi != null || vo != null
        }

        override fun next(): T {
            if (!hasNext()) throw NoSuchElementException()

            val v: T?
            if (vi != null && (vo == null || comparator == null || comparator.compare(vi, vo) <= 0)) {
                v = vi
                vi = i.nextOrNull()
            } else {
                v = vo
                vo = o.nextOrNull()
            }

            return v!!
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's see it in action:

val list1 = listOf(1, 3, 5)
val list2 = listOf(2, 4, 6)

var iterator1 = list1.iterator()
var iterator2 = list2.iterator()

// Int: 1, 3, 5, 2, 4, 6
val merged = iterator1.merge(iterator2)

iterator1 = list1.iterator()
iterator2 = list2.iterator()

// Int: 1, 2, 3, 4, 5, 6
val sorted = iterator1.merge(iterator2, Comparator.comparingInt<Int> { i -> i })
Enter fullscreen mode Exit fullscreen mode

So, we just merged and even sorted two iterators šŸ¤Æ. But... As Leonardo says...
We have to go deeper

Sure thing:

object IteratorUtil {

    fun <T> merge(vararg iterators: Iterator<T>): Iterator<T> {
        var i = Collections.emptyIterator<T>()
        iterators.forEach { i = i.merge(it) }
        return i
    }
}
Enter fullscreen mode Exit fullscreen mode

Whaaaaat šŸ¤ÆšŸ¤ÆšŸ¤Æ

Jokes aside in our case this merge exercises are very needed even though they look scary. You can always see this code in action in our Github projects.

In the post I just wanted to show all the power and beauty of Kotlin extension functions and maybe give you some motivation to use them.

šŸ‘Øā€šŸ’» Happy coding,
Your SIP3 team.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
agafox
Oleg Agafonov

Posted on November 17, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related