Skip to content
ioob.dev
Go back

Kotlin Beginner Part 5 — Collections and Lambdas

· 3 min read
Kotlin Series (5/12)
  1. Kotlin Beginner Part 1 — Variables and Types
  2. Kotlin Beginner Part 2 — Conditionals and Loops
  3. Kotlin Beginner Part 3 — Functions
  4. Kotlin Beginner Part 4 — Classes and Objects
  5. Kotlin Beginner Part 5 — Collections and Lambdas
  6. Kotlin Part 6 — Null Safety Advanced
  7. Kotlin Part 7 — Generics
  8. Kotlin Part 8 — sealed class and enum
  9. Kotlin Part 9 — Coroutines Basics
  10. Kotlin Part 10 — Coroutines Advanced
  11. Kotlin Part 11 — DSL and Advanced Functions
  12. Kotlin Part 12 — Practical Patterns
Table of contents

Table of contents

Creating Collections

Kotlin collections come in two flavors: immutable and mutable. As we discussed in Part 1 with val/var, immutability is the default — and the same philosophy applies to collections.

// Immutable — cannot add/remove elements after creation
val fruits = listOf("Apple", "Banana", "Strawberry")
val numbers = setOf(1, 2, 3, 3)  // Duplicates removed -> {1, 2, 3}
val scores = mapOf("Korean" to 90, "Math" to 85)

// Mutable — can add/remove elements
val mutableFruits = mutableListOf("Apple", "Banana")
mutableFruits.add("Strawberry")
mutableFruits.remove("Banana")

You cannot call add() on a list created with listOf(). It results in a compile error. If modification is needed, you must create it with mutableListOf() from the start.

Thanks to this design, you don’t have to worry about the original being modified when passing a collection to a function. It prevents unintended side effects.

Lambda Expressions

In Kotlin, functions are first-class citizens. They can be stored in variables and passed as arguments to other functions.

// Store a lambda in a variable
val double = { x: Int -> x * 2 }
println(double(5))  // 10

// Pass as an argument to a function
fun calculate(x: Int, operation: (Int) -> Int): Int {
    return operation(x)
}

println(calculate(5, double))       // 10
println(calculate(5) { it * 3 })    // 15

{ x: Int -> x * 2 } is a lambda expression. The left side of the arrow (->) is the parameter, and the right side is the body.

When there’s only one parameter, you can abbreviate it with it. The last line { it * 3 } is an example of this.

filter, map, forEach

Using functional APIs instead of for loops makes collection processing much more concise.

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// Filter only even numbers
val evens = numbers.filter { it % 2 == 0 }
println(evens)  // [2, 4, 6, 8, 10]

// Double each element
val doubled = numbers.map { it * 2 }
println(doubled)  // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// Chaining — filter evens then double them
val result = numbers
    .filter { it % 2 == 0 }
    .map { it * 2 }
println(result)  // [4, 8, 12, 16, 20]

// Iteration
numbers.forEach { print("$it ") }  // 1 2 3 4 5 6 7 8 9 10

filter keeps only elements that match the condition, and map transforms each element. Chaining these two together lets you write complex data processing as easily readable code.

Visualizing the chaining as a pipeline where data is transformed step by step looks like this.

flowchart LR
    Input["[1,2,3,4,5,6,7,8,9,10]"] --> F["filter { it % 2 == 0 }"]
    F --> Mid["[2,4,6,8,10]"]
    Mid --> M["map { it * 2 }"]
    M --> Out["[4,8,12,16,20]"]

A few more commonly used functions are worth knowing.

val names = listOf("Kim", "Lee", "Park", "Kim", "Choi")

println(names.first())          // Kim
println(names.last())           // Choi
println(names.distinct())       // [Kim, Lee, Park, Choi]
println(names.sorted())         // [Choi, Kim, Kim, Lee, Park]
println(names.any { it == "Lee" })   // true
println(names.count { it == "Kim" }) // 2

Working with Maps

Maps support the same functional APIs.

val scores = mapOf("Korean" to 90, "Math" to 85, "English" to 92)

// Iteration
for ((subject, score) in scores) {
    println("$subject: $score")
}

// Filter scores 90 and above
val high = scores.filter { (_, score) -> score >= 90 }
println(high)  // {Korean=90, English=92}

// Extract values and compute the average
val avg = scores.values.average()
println(avg)  // 89.0

With destructuring declarations (subject, score), you can extract the key and value directly. Unused variables can be ignored with _.

Scope Functions

Kotlin has five scope functions: let, apply, run, also, and with. They can be confusing at first, but knowing just the three most commonly used in practice is enough.

let — Null Check + Transformation

val name: String? = "Kotlin"

// Execute the block only when name is not null
name?.let {
    println("Name: $it, Length: ${it.length}")
}

// Return the transformed result
val length = name?.let { it.length } ?: 0

?.let is an idiomatic pattern for safely handling Nullable variables. it refers to the object inside the block.

apply — Object Initialization

data class Config(
    var host: String = "",
    var port: Int = 0,
    var debug: Boolean = false
)

val config = Config().apply {
    host = "localhost"
    port = 8080
    debug = true
}

apply returns the object itself. Since this inside the block refers to the object, you can set properties directly. It’s useful for initializing objects without the builder pattern.

run — Compute a Result from an Object

val greeting = "Hello, Kotlin!".run {
    println("Original: $this")
    this.uppercase()  // This value is returned
}
println(greeting)  // HELLO, KOTLIN!

run is similar to apply, but returns the last line of the block instead of the object itself. Use it when you need to compute a different value using an object.

Summary

Functionthis/itReturn ValuePrimary Use
letitBlock resultNull check, transformation
applythisThe object itselfObject initialization
runthisBlock resultComputation from object

There are also also and with, but the above three cover most situations. You can look them up later when needed.


This concludes the basics section of the Kotlin beginner series. Over five parts, we covered fundamental syntax: variables, control flow, functions, classes, and collections. This provides a sufficient foundation to start a simple Kotlin project.

Starting from the next part, we dive into intermediate topics. Let’s explore null safety in greater depth.

-> Part 6: Null Safety Advanced


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Kotlin Beginner Part 4 — Classes and Objects
Next Post
Go Basics Part 1 — Variables, Constants, and Types