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
| Function | this/it | Return Value | Primary Use |
|---|---|---|---|
let | it | Block result | Null check, transformation |
apply | this | The object itself | Object initialization |
run | this | Block result | Computation 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.




Loading comments...