December Code

Kotlin collections interview questions, with answers

Kotlin's collections library is one of the biggest improvements over Java: separate read-only and mutable interfaces, and a large set of functional operations available directly on every collection. Interview questions check that you can use those operations fluently, choose between a list chain and a lazy sequence, and know the details, such as how grouping and sorting by several keys work.

The answers below cover the collection types, transformations, sequences, grouping, arrays, sorting and maps, with code compiled with kotlinc 2.3 and run on the JVM. Then take the free Kotlin diagnostic — ten questions across every Kotlin topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.What is the difference between List and MutableList in Kotlin?

    In short: List is a read-only interface with no functions that add or remove elements; MutableList extends it with add, remove and set.

    Kotlin splits each collection type in two: List, Set and Map offer only reading, while MutableList, MutableSet and MutableMap add modifying functions. listOf creates a List, and mutableListOf a MutableList, so += below appends to grow but would not compile for fixed. Functions should take the read-only type when they only read, which documents that they will not change the caller's collection. Both are backed by ordinary Java collections at run time, typically an ArrayList. Truly immutable collections, which no reference can modify, come from the kotlinx.collections.immutable library.

    val fixed = listOf(1, 2)
    val grow = mutableListOf(1, 2)
    grow += 3
    println("$fixed $grow")
    // [1, 2] [1, 2, 3]
  2. 2.How do map, filter and similar operations work on Kotlin collections?

    In short: They are extension functions that each return a new collection, so they can be chained into a pipeline, ending in a result such as sum or first.

    filter keeps elements matching a predicate, map transforms each element, and terminal functions such as sum, count, any, first and fold reduce the result to a value. Each step on a List builds a new List, which keeps the code simple and the original untouched. In the code, filter keeps prices of at least 100, map applies a 10 percent discount with integer arithmetic, and sum adds 108 and 270. Related functions include flatMap for nested collections, mapIndexed, filterIsInstance, sumOf with a selector, and zip to pair two collections.

    val p = listOf(120, 45, 300)
    val total = p
        .filter { it >= 100 }
        .map { it * 9 / 10 }
        .sum()
    println(total)
    // 378
  3. 3.What is the difference between a sequence and a list in Kotlin?

    In short: List operations are eager and build an intermediate list at each step; a Sequence is lazy, passing each element through the whole chain and stopping when the result is known.

    A chain of list operations processes the whole collection at every step, which is simple and fast for small collections. A Sequence, obtained with asSequence(), sequenceOf or generateSequence, evaluates lazily: nothing runs until a terminal operation such as toList or first, and then elements flow through the chain one at a time. That avoids intermediate lists on long chains, stops early when possible, and allows infinite sequences, as below, where doubling from 1 stops at the first value over 100. For short collections, lists are usually faster because sequences add per-element overhead.

    val f = generateSequence(1) {
        it * 2
    }.first { it > 100 }
    println(f)
    // 128
  4. 4.How do groupBy and partition work in Kotlin?

    In short: groupBy builds a map from a key to the list of elements with that key; partition splits a collection into a pair of lists, those matching a predicate and the rest.

    groupBy { it[0] } below groups words by their first letter, returning a Map<Char, List<String>> in which g['a'] is every word starting with a, in their original order. partition returns a Pair of two lists, which is usually destructured on the spot, as val (a, rest) = ... does. Related functions are associateBy, which builds a map from a key to a single element, associate for key-value pairs, groupingBy with eachCount to count occurrences, and chunked and windowed for fixed-size groups. They replace most hand-written loops that fill maps.

    val w = "an ox ap".split(" ")
    val g = w.groupBy { it[0] }
    println(g['a'])
    // [an, ap]
    val (a, rest) =
        w.partition { 'a' in it }
    println(rest)
    // [ox]
  5. 5.What is the difference between Array and List in Kotlin?

    In short: An Array has a fixed size and mutable elements, like a Java array; a List is a collection interface that can be read-only or mutable and can grow.

    arrayOf(3, 1, 2) creates an Array<Int>, whose size is fixed at creation but whose elements can be replaced, as arr[0] = 9 does, and which can be sorted in place. Arrays of primitives have dedicated types, such as IntArray from intArrayOf, which store raw ints without boxing and are the right choice for performance-sensitive numeric code. Lists are usually preferred elsewhere: they have read-only variants, can grow when mutable, and compare structurally with ==, whereas two arrays with the same contents are equal only with contentEquals. Most collection functions, such as map and sum, work on both.

    val arr = arrayOf(3, 1, 2)
    arr[0] = 9
    arr.sort()
    println(arr.joinToString())
    // 1, 2, 9
    val ints = intArrayOf(1, 2)
    println(ints.sum())
    // 3
  6. 6.How do you sort a collection by several keys in Kotlin?

    In short: Use sortedWith with compareBy, listing the keys in order of priority; later keys only break ties in earlier ones.

    sorted() uses the elements' natural order and sortedBy { it.age } sorts by one key, while sortedWith(compareBy({ it.age }, { it.name })) sorts by age and then, among people of the same age, by name, as the code does. thenBy and thenByDescending build the same comparators step by step, and sortedByDescending reverses the order. These functions return new lists; the in-place versions, sort and sortBy, exist only on mutable lists. Kotlin's sorts are stable, so elements that compare equal keep their original relative order.

    data class P(
        val name: String,
        val age: Int,
    )
    
    fun main() {
        val ps = listOf(P("b", 30),
            P("a", 30), P("c", 25))
        val s = ps.sortedWith(
            compareBy({ it.age },
                { it.name }))
        println(s.map { it.name })
        // [c, a, b]
    }
  7. 7.What are the common Map operations in Kotlin?

    In short: Read with m[key], which returns null for a missing key, write with m[key] = v on a mutable map, and use getOrPut, getOrElse and merge for defaults and updates.

    Indexing a map returns a nullable value, so s["c"] ?: 0 below gives a default for a missing key without an exception; getValue throws instead when a missing key is a bug. On a MutableMap, m[key] = v inserts or replaces, getOrPut(key) { ... } inserts a computed value only when the key is absent, which is the idiom for caches, and merge, from the JVM's Map interface, combines a new value with an existing one, here adding 1 to the count stored for a. Maps are created with mapOf or mutableMapOf from pairs built with to, and keep insertion order.

    val s = mutableMapOf("a" to 2)
    s["b"] = 5
    s.merge("a", 1, Int::plus)
    println(s)
    // {a=3, b=5}
    println(s["c"] ?: 0)
    // 0

How the diagnostic asks it

One question from the Kotlin bank, exactly as a sitting would show it. The bank has 3 on collections & sequences and 30 across Kotlin.

Collections & Sequences · mediumKT-019

What does this Kotlin code print?

val r = sequenceOf(1, 2, 3, 4)
    .map { print("m$it "); it * 2 }
    .filter { print("f$it "); it > 4 }
    .first()
println(r)
  1. 1m1 m2 m3 m4 f2 f4 f6 f8 6
  2. 2m1 m2 m3 m4 f2 f4 f6 6
  3. 3m1 f2 m2 f4 m3 f6 6correct
  4. 46

Sequence operations are lazy and element by element: nothing runs until a terminal operation, here first(), asks for an element, and then each element travels through map and filter before the next one starts. 1 maps to 2, which fails the filter; 2 maps to 4, which fails; 3 maps to 6, which passes, and first() stops there, so 4 is never processed. The output is m1 f2 m2 f4 m3 f6 6. m1 m2 m3 m4 first is how a List chain runs, each step over the whole collection. The option without f8 still maps everything first. The last option forgets the lambdas' prints.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 Kotlin questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated