zsmb.coBeta 3



Top 10 Kotlin Stack Overflow questions, pt 1 - decisions, decisions

2018.04.24. 8h

This is the written version of a talk I gave recently, and since it was quite long, this is the first of three articles about it. When you're done with this one, read the second and third parts as well!


I am currently defending the third place on the top users list of the Kotlin tag on StackOverflow, and I wanted to make use of the bragging rights this gives me while I can. The best way I found is to have a look at some of the most frequently asked questions about Kotlin on StackOverflow.

1. Array<Int> vs IntArray

What's the difference between Array<Int> and IntArray?

This is a simple one to start with.

Array<Int> uses the generic Array class, which can store a fixed number of elements for any T type. When using this with the Int type parameter, what you end up in the bytecode is an Integer[] instance, in Java parlance.

This is what you get when you use the generic arrayOf method to create an array:

val arrayOfInts: Array<Int> = arrayOf(1, 2, 3, 4, 5)

IntArray is a special class that lets you use a primitive array instead, i.e. int[] in Java terms. (There are similarly named classes for the other primitive types as well, such as ByteArray, CharArray, etc.)

These can be created with their own (also non-generic, like the class itself) intArrayOf factory method:

val intArray: IntArray = intArrayOf(1, 2, 3, 4, 5)
When to use which one?

Use IntArray by default. Primitive arrays are more performant, as they don't require boxing for every element. They are also easier to create - an Array<Int> requires a non-null value for each of its indexes, while IntArray initializes them automatically to 0 values. Here's an example of this, using their constructors:

val intArray = IntArray(10)
val arrayOfInts = Array<Int>(5) { i -> i * 2 }

Use Array<Int> when you're forced to use the Array class by an API, or if you need to store potentially null values, which an Array<Int?> is of course able to do. If you need to create an Array<T?>, the simplest way is using the arrayOfNulls function of the standard library:

val notActualPeople: Array<Person?> = arrayOfNulls<Person>(13)

2. Iterable vs Sequence

What's the difference between an Iterable and a Sequence?

Iterable is mapped to the java.lang.Iterable interface on the JVM, and is implemented by commonly used collections, like List or Set. The collection extension functions on these are evaluated eagerly, which means they all immediately process all elements in their input and return a new collection containing the result.

Here's a simple example of using the collection functions to get the names of the first five people in a list whose age is at least 21:

data class Person(val name: String, val age: Int)

fun getPeople() = listOf(
        Person("Jane", 25),
        Person("Sally", 39),
        Person("Joe", 44),
        Person("Jimmy", 15),
        Person("Samantha", 56),
        Person("Claire", 47),
        Person("Susan", 27)
)

fun main(args: Array<String>) {
//sampleStart
    val people: List<Person> = getPeople()
    val allowedEntrance = people
            .filter { it.age >= 21 }
            .map { it.name }
            .take(5)
//sampleEnd
    println(allowedEntrance)
}

First, the age check is done for every single Person in the list, with the result put in a brand new list. Then, the mapping to their names is done for every Person who remained after the filter operator, ending up in yet another new list (this is now a List<String>). Finally, there's one last new list created to contain the first five elements of the previous list.

In contrast, Sequence is a new concept in Kotlin to represent a lazily evaluated collection of values. The same collection extensions are available for the Sequence interface, but these immediately return Sequence instances that represent a processed state of the date, but without actually processing any elements. To start processing, the Sequence has to be terminated with a terminal operator, these are basically a request to the Sequence to materialize the data it represents in some concrete form. Examples include toList, toSet, and sum, to mention just a few. When these are called, only the minimum required number of elements will be processed to produce the demanded result.

Transforming an existing collection to a Sequence is pretty straightfoward, you just need to use the asSequence extension. As mentioned above, you also need to add a terminal operator, otherwise the Sequence will never do any processing (again, lazy!).

data class Person(val name: String, val age: Int)

fun getPeople() = listOf(
        Person("Jane", 25),
        Person("Sally", 39),
        Person("Joe", 44),
        Person("Jimmy", 15),
        Person("Samantha", 56),
        Person("Claire", 47),
        Person("Susan", 27)
)

fun main(args: Array<String>) {
//sampleStart
    val people: List<Person> = getPeople()
    val allowedEntrance = people.asSequence()
        .filter { it.age >= 21 }
        .map { it.name }
        .take(5)
        .toList()
//sampleEnd
    println(allowedEntrance)
}

In this case, the Person instances in the Sequence are each checked for their age, if they pass, they have their name extracted, and then added to the result list. This is repeated for each person in the original list until there are five people found. At this point, the toList function returns a list, and the rest of the people in the Sequence are not processed.

There's also something extra a Sequence is capable of: it can contain an infinite number of items. With this in perspective, it makes sense that operators work the way they do - an operator on an infinite sequence could never return if it did its work eagerly.

As an example, here's a sequence that will generate as many powers of 2 as required by its terminal operator (ignoring the fact that this would quickly overflow):

fun main(args: Array<String>) {
//sampleStart
    generateSequence(1) { n -> n * 2 }
            .take(20)
            .forEach(::println)
//sampleEnd
}
Which one should I use?

Use Iterable by default. You will be creating intermediary collections, but these generally don't affect performance too badly. In fact, for small collections, they might be faster than the overhead that using a Sequence introduces.

Use a Sequence if you need to handle an infinite number of elements - this is something they are uniquely capable of doing. Consider a Sequence if you have very large collections to manipulate, and expect a performance gain from doing so lazily - perhaps you know that there are elements you won't need to process. As with always with performance advice, you'll have to benchmark your specific code to see if this really is the right choice for you.

Finally, use a Stream if you are going to be interoperating with Java code that already uses them. They work just fine in Kotlin (while Sequences do not work in Java).

3. Iteration with indexes

How can/should I iterate over a collection of items?

Here's probably everyone's first idea of how to do this after seeing the for loop syntax with ranges:

fun main(args: Array<String>) {
    val args = arrayOf("arg1", "arg2", "arg3")
//sampleStart
    for (i in 0..args.size - 1) {
        println(args[i])
    }
//sampleEnd
}

Then you might find out that Array has a lastIndex extension property that's easier to read:

fun main(args: Array<String>) {
    val args = arrayOf("arg1", "arg2", "arg3")
//sampleStart
    for (i in 0..args.lastIndex) {
        println(args[i])
    }
//sampleEnd
}

Then you realize that you don't actually need the last index, you just need to have an open ended range, which is what until creates:

fun main(args: Array<String>) {
    val args = arrayOf("arg1", "arg2", "arg3")
//sampleStart
    for (i in 0 until args.size) {
        println(args[i])
    }
//sampleEnd
}

Of course, then again you'll find out that you can get this same range with the indices extension property:

fun main(args: Array<String>) {
    val args = arrayOf("arg1", "arg2", "arg3")
//sampleStart
    for (i in args.indices) {
        println(args[i])
    }
//sampleEnd
}

But you can also iterate the collection directly instead of a range of indexes, with this syntax:

fun main(args: Array<String>) {
    val args = arrayOf("arg1", "arg2", "arg3")
//sampleStart
    for (arg in args) {
        println(arg)
    }
//sampleEnd
}

Or you can go a bit functional and use the forEach function, passing a lambda to it that will do the work on each element:

fun main(args: Array<String>) {
    val args = arrayOf("arg1", "arg2", "arg3")
//sampleStart
    args.forEach { arg ->
        println(arg)
    }
//sampleEnd
}

These are all very, very similar in terms of the generated code - in this case of iterating over an Array, they all increment an index variable and get elements by the index in a loop.

But if we were iterating a List instead, the last two solutions would instead use an Iterator under the hood, while the rest would still make a get call with each index. The Iterator approach here has the potential of being more efficient for certain collections1.

1 For example, iterating over a LinkedList would be an O(n^2) operation, as a LinkedList has O(n) lookup times. It wouldn't matter for an ArrayList, which has O(1) lookups, since it uses an Array to store its items.

Using the indices extension property is a close third in the race - it uses a for loop and looks up elements by index, but it has fairly clean bytecode compared to the three other methods before it.

What if I need the current item's index as well?

Firstly, there's the withIndex extension function that returns an Iterable of objects that can be destructured into the current index and element:

for ((index, arg) in args.withIndex()) {
    println("$index: $arg")
}

However, there's also the forEachIndexed function that calls the provided lambda for each index and argument.

args.forEachIndexed { index, arg ->
    println("$index: $arg")
}

Both of these functions use iterators for lists, but while the withIndex() solution uses an iterator for arrays too, the forEachIndexed is nicely optimized and uses indexes in that case.


The next article covering SAM conversions and replacing Java's statics is waiting for you here.