JavaRush/Courses/Kotlin SELF/map and filter — transformation and selection as a baseli...

map and filter — transformation and selection as a baseline style

Open

1. Introduction

When you’re just starting to program, a for loop feels like a universal screwdriver: you can walk through a list, count something, print something, “redo” something. But in real tasks, loops start to grow: an if appears inside, then another if inside that, somewhere a result is accumulated, somewhere something is printed, and all of this turns into “spaghetti” that’s hard to read, test, and change.

The idea of collection operations in Kotlin is to make your code look like a data processing pipeline: first we select what we need, then we transform it, then we use the result. Kotlin’s standard library directly supports this style: map() for transformation and filter() for selection are the basic “building blocks” of a collection transformation pipeline.

From this point on, a useful habit sounds like this: if you want to get a new list, first think not about a loop, but about map/filter.

2. map: turning each element into something else

map is an operation that takes a source collection and computes a new value for each element, putting the results into a new list. In Kotlin this is called a mapping transformation. It’s important that map() applies a lambda to each element in order and returns a List of results in the same order.

Imagine a stamp on a factory line: parts (list elements) move along the conveyor belt, and map puts its own “transformation stamp” on each part, after which the parts are placed into a new box (a new list).

Minimal example: numbers → squares

Let’s start with a very simple illustration so your brain doesn’t overheat:

fun main() {
    val xs = listOf(1, 2, 3)

    val squares = xs.map { x -> x * x }

    println(xs)       // [1, 2, 3]
    println(squares)  // [1, 4, 9]
}

Here it’s important to notice two things. First, xs did not change: map does not “redo the list”, it builds a new one. Second, the result type can differ from the input type: that’s the main fun of map.

The result type can change: strings → lengths

map is very often used to pull out “some field” or a “derived value” from each element:

fun main() {
    val names = listOf("Ann", "Bob", "Chris")

    val lengths = names.map { name -> name.length }

    println(lengths) // [3, 3, 5]
}

The input was List<String>, the output became List<Int>. That’s normal and expected: map does not have to return the same type; it “recolors” elements.

it or a named parameter

In short lambdas it’s convenient to use it so you don’t have to write the parameter name. But as soon as there’s more than one thought inside the lambda, it starts to look like “it... well... it...”. It’s better to give it a name.

Compare:

fun main() {
    val raw = listOf("  Ann ", " Bob  ")

    val cleaned1 = raw.map { it.trim() }
    val cleaned2 = raw.map { s -> s.trim() }

    println(cleaned1) // [Ann, Bob]
    println(cleaned2) // [Ann, Bob]
}

Both variants are correct. It’s just that in the second case it’s easier for the reader if later you want to add more steps.

Important point: map is not for printing

Sometimes beginners write: “let me both transform and print right inside map.” You can do that, but it’s a bad habit: you’re mixing computation and action. In Kotlin the style is usually: map/filter compute data, and printing is a forEach at the end of the chain.

Technically, this will work:

fun main() {
    val xs = listOf(1, 2, 3)

    val ys = xs.map { x ->
        println("Processing $x") // Processing 1 / 2 / 3
        x * 10
    }

    println(ys) // [10, 20, 30]
}

But now map does two things at once: it transforms and prints (a side effect). That means it’s harder to debug and harder to reuse.

3. filter: keeping only what matches

If map is a “machine that reworks every part”, then filter is a “sieve”: we let through only what satisfies the condition and throw away the rest. In Kotlin, filtering is defined by a predicate — a function of type (T) -> Boolean that answers the question “keep this element or remove it?”

filter() returns a new collection with only the matching elements, and the source collection does not change. This is a very important idea because it makes the code predictable.

Minimal example: keep only long words

fun main() {
    val words = listOf("a", "home", "to", "kotlin")

    val longWords = words.filter { it.length >= 4 }

    println(longWords) // [home, kotlin]
}

Inside filter we don’t “add elements”; we simply say: “this one matches (true), and this one doesn’t (false).”

A more real-life example: keep only positive numbers

fun main() {
    val xs = listOf(-2, 0, 5, -1, 7)

    val positives = xs.filter { x -> x > 0 }

    println(positives) // [5, 7]
}

Notice that filter does not change the element type: it was Int and it stays Int. That’s the key difference from map.

filter also works on Map

Although today we focus on the baseline style for lists, it’s useful to know: filter exists for Map as well, and the result remains a Map. In the lambda when filtering a map, you can accept (key, value) via destructuring.

A mini example “just to see”:

fun main() {
    val scores = mapOf("Ann" to 10, "Bob" to 3, "Chris" to 8)

    val good = scores.filter { (_, score) -> score >= 8 }

    println(good) // {Ann=10, Chris=8}
}

We won’t build separate scenarios around this now; just remember: it’s the same “sieve” principle, only the elements are key-value pairs.

4. map and filter: comparison, chains, and the “pipeline”

After the first examples, many people get confused: “wait, but filter also does something to the list... and map also kind of selects something...”. Let’s lock in the difference carefully.

map vs filter: a quick-check table

Operation Main question The lambda returns What the operation returns Does the element type change?
map { ... }
“What should each element be turned into?” a value (for example,
Int
,
String
,
Double
)
List<R>
yes, it can
filter { ... }
“Keep the element or throw it away?”
Boolean
List<T>
(a subset)
no

If you catch yourself thinking “I want a new list” — that’s fine for both operations. But if you want a new list in a different shape, that’s usually map. If you want to shrink a list based on a condition, that’s usually filter.

Chains: the order of steps is part of the logic

When a chain like .filter { ... }.map { ... } appears in code, it’s useful to read it as a list of data-processing steps top to bottom: “take the source list → remove what’s not needed → transform what remains”. Kotlin directly supports this style in its basic syntax: a chain of filtering, transformation, and final action looks very natural.

Here’s a classic example of “cleaning up strings”:

fun main() {
    val raw = listOf("  Ann ", "  ", "Bob", " Chris")

    val cleaned = raw
        .map { it.trim() }
        .filter { it.isNotEmpty() }

    println(cleaned) // [Ann, Bob, Chris]
}

The logic matters here: we do trim first, because the string " " after trim() turns into "", and only after that can we correctly filter out empty strings.

Sometimes the filter condition is more convenient to check on the transformed value. For example, if you convert strings to lowercase so the comparison is case-insensitive:

fun main() {
    val words = listOf("Kotlin", "java", "KOTLIN", "Python")

    val onlyKotlin = words
        .map { it.lowercase() }
        .filter { it == "kotlin" }

    println(onlyKotlin) // [kotlin, kotlin]
}

We got two "kotlin" elements because there were two of them originally in different casing. Here map does normalization, and filter works on the normalized data.

Mental model: a step-by-step “pipeline”

When you see .filter { ... }.map { ... }.forEach { ... }, it’s useful to think of it as a factory pipeline. This isn’t philosophy; it’s very practical: it’s easier for the brain when it sees “steps”. Kotlin encourages this style because collection transformation operations are built around the idea of “create a new collection by rules”.

flowchart LR
    A["Source list (List⟨T⟩)"] --> B["filter { ... }
selection"] B --> C["map { ... }
transformation"] C --> D["Result (List⟨R⟩)"] D --> E["forEach { ... }
use, print"]

For now, take this literally: filter and map are about data and results, while forEach is about actions.

5. map and filter in a console project

From today on, we’ll use a very practical rule: any “show a list” commands in a console app almost always consist of the steps “select → transform → output”. Our practical project (which we started around collections) can be imagined as a simplest CLI expense tracker: we store operations in a MutableList, add a record, print the list, delete by index. For now we’re not introducing classes (that will be later), so we’ll store operations as a Triple.

Let one operation look like this: (title, category, amount).

Data and a small helper for nice display

First, let’s make a small function for formatting a record. Yes, this is also a “transformation” — and it’s a great target for map.

fun formatExpense(e: Triple<String, String, Int>): String {
    val (title, category, amount) = e
    return "[$category] $title: $amount"
}

fun main() {
    val expenses = mutableListOf(
        Triple("Coffee", "food", 250),
        Triple("Book", "education", 1200),
        Triple("Taxi", "transport", 600)
    )

    val lines = expenses.map(::formatExpense)
    lines.forEach(::println)
    // [food] Coffee: 250
    // [education] Book: 1200
    // [transport] Taxi: 600
}

Notice the idea: map turns a list of “complex triples” into a list of “ready-to-print strings”, and we do the printing with forEach. Responsibilities are neatly separated.

The list food command: filter by category

Now let’s add filtering. Imagine the user entered a category, and we want to print only the items from that category.

fun main() {
    val expenses = listOf(
        Triple("Coffee", "food", 250),
        Triple("Burger", "food", 500),
        Triple("Book", "education", 1200)
    )

    val category = "food"

    expenses
        .filter { (_, cat, _) -> cat == category }
        .map(::formatExpense)
        .forEach(::println)
    // [food] Coffee: 250
    // [food] Burger: 500
}

Here destructuring (_, cat, _) helps the condition read naturally: “keep only those where cat == category”.

Category from input and “dirty” data

Suppose the user enters " Food " with spaces and different casing. We’re not doing complex validation yet; we just normalize the input and compare in lowercase. You’ll get a “chain on a chain”, but it’s still readable if the steps are short.

fun main() {
    val expenses = listOf(
        Triple("Coffee", "food", 250),
        Triple("Book", "education", 1200)
    )

    print("Category: ")
    val raw = readln()

    val category = raw.trim().lowercase()

    expenses
        .filter { (_, cat, _) -> cat.lowercase() == category }
        .map(::formatExpense)
        .forEach(::println)
}

The key here is not to overcomplicate it: we normalized the input separately, then wrote a readable pipeline. If you try to “cram everything inside filter”, it becomes hard to read.

6. Typical mistakes when working with map and filter

Mistake #1: trying to use map as a “printing loop”.
A very common story: “I’m iterating over all elements, so map fits.” Formally yes, but the point of map is different: it should build a useful list of results. If you print inside map, you add a side effect and mix responsibilities. Usually it’s better to leave printing for the final forEach, and keep map “pure”.

Mistake #2: confusing “what the lambda returns”.
For filter, the lambda must return Boolean. For map, the lambda must return a value of the new type (or the same type). If you accidentally returned a string in filter, the compiler will stop you, but sometimes the error looks scary. A useful self-check: “Am I answering ‘yes/no’ right now?” — then it’s filter. “Am I computing a new value right now?” — then it’s map.

Mistake #3: expecting filter or map to change the source collection.
After an imperative style (where you’re used to doing list[i] = ...) it’s easy to subconsciously expect map to “redo the list”. But by contract these operations create a new result, and the source collection stays the same. That’s helpful: fewer surprises, less accidentally broken state.

Mistake #4: overly long lambdas inside map/filter.
When code is small, you want to write everything inside one lambda. But as soon as you have 8 lines inside map with if/else, temporary variables, and formatting, the chain stops being a chain and turns into a mini novel. In such cases it’s easier to move the transformation into a separate function (as we did with formatExpense) and use ::formatExpense — and the code becomes readable again.

Mistake #5: the wrong step order in a chain.
Sometimes you filter by a condition that only makes sense after a transformation (or vice versa). A typical example is strings with spaces: if you filter isNotEmpty() first, the string " " will pass the filter even though you don’t need it. That’s why step order is not “cosmetics” but part of the logic. If the data requires normalization, normalize first (map { it.trim() }), then select (filter { it.isNotEmpty() }).

1
Task
Kotlin SELF,  22 level,  1 lecture
Locked
Weather around the world
Weather around the world
1
Task
Kotlin SELF,  22 level,  1 lecture
Locked
Hint Filter
Hint Filter
1
Task
Kotlin SELF,  22 level,  1 lecture
Locked
Clean tags
Clean tags
1
Task
Kotlin SELF,  22 level,  1 lecture
Locked
Price showcase
Price showcase
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet