Top 10 Kotlin Stack Overflow questions, pt 1 - decisions, decisions
2018-04-24 • Márton Braun
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 Sequence
s 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.
You might also like...
Top 10 Kotlin Stack Overflow questions, pt 2 - the big ones
In the second part of the series, I'm covering various possible issues with using SAM conversions, as well as how to create "static" things in Kotlin.
Top 10 Kotlin Stack Overflow questions, pt 3 - nulls and such
The third and final part covers topics of nullability and some small extras to wrap up the series.
How I Finally Memorized Modifier Ordering in Compose
For the longest time, I proudly had no idea of how Modifier ordering works, and would just guess and then guess again when something didn't look quite right. Here's how I finally ended up remembering how the ordering works.
The Other Side of Stack Overflow
My experiences and thoughts after using Stack Overflow from the other side for over a year now, answering questions mostly under the Kotlin tag.