Contactez-nous

Opérations courantes : `forEach`, `map`, `filter`

Apprenez à utiliser les fonctions d'ordre supérieur forEach, map et filter pour traiter les collections en Kotlin de manière fonctionnelle et concise.

Introduction : traiter les collections de manière fonctionnelle

Maintenant que nous avons compris ce que sont les collections (`List`, `Set`, `Map`) et les expressions lambda, nous pouvons explorer la puissance qui émerge de leur combinaison. La bibliothèque standard de Kotlin fournit une multitude de fonctions d'extension pour les collections, conçues pour effectuer des opérations courantes de manière déclarative et fonctionnelle. Au lieu d'écrire des boucles `for` manuelles avec des conditions `if` imbriquées pour manipuler les données, nous utilisons ces fonctions qui prennent des lambdas en argument pour spécifier *quoi* faire, plutôt que *comment* le faire.

Parmi ces nombreuses fonctions, trois sont absolument fondamentales et constituent la base de la plupart des traitements de collections : `forEach`, `filter`, et `map`. Maîtriser ces trois opérations vous permettra de manipuler des données de manière beaucoup plus concise, lisible et moins sujette aux erreurs que les approches impératives traditionnelles.

`forEach` : exécuter une action pour chaque élément

L'opération `forEach` est la plus simple. Son but est d'exécuter une action donnée (définie par une lambda) pour chaque élément de la collection, dans l'ordre (pour les collections ordonnées comme `List`). Elle ne retourne rien d'utile (son type de retour est `Unit`) et est principalement utilisée pour ses effets de bord, comme l'affichage d'éléments, la modification d'une variable externe (avec prudence), ou l'appel d'une autre fonction pour chaque élément.

La lambda passée à `forEach` prend généralement un seul argument : l'élément courant de la collection. On utilise très souvent le paramètre implicite `it`.

fun main() {
    val languages = listOf("Kotlin", "Java", "Python", "JavaScript")

    println("Langages disponibles (avec forEach) :")
    // La lambda { println("- $it") } est exécutée pour chaque langage
    languages.forEach { 
        println("- $it") 
    }

    // Equivalent plus verbeux avec une boucle for classique
    println("\nLangages disponibles (avec boucle for) :")
    for (lang in languages) {
        println("- $lang")
    }

    val numbers = setOf(1, 5, 2)
    var sum = 0
    // Utilisation pour un effet de bord (calcul de somme)
    numbers.forEach { sum += it } 
    println("\nSomme des nombres : $sum") // Affiche 8
}

`forEach` est utile pour les actions simples à appliquer à tous les éléments, mais elle n'est pas conçue pour transformer ou filtrer la collection elle-même.

`filter` : sélectionner des éléments selon une condition

L'opération `filter` permet de créer une nouvelle collection contenant uniquement les éléments de la collection originale qui satisfont une certaine condition (un prédicat).

Elle prend en argument une lambda qui doit retourner une valeur `Boolean`. Cette lambda (le prédicat) est appelée pour chaque élément de la collection originale. Si la lambda retourne `true` pour un élément, cet élément est inclus dans la nouvelle collection résultante. Si elle retourne `false`, l'élément est ignoré.

Crucialement, `filter` ne modifie pas la collection originale ; elle retourne une nouvelle instance (généralement une `List`) contenant les éléments filtrés.

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

    // Filtrer pour ne garder que les nombres pairs
    // La lambda { it % 2 == 0 } retourne true si 'it' est pair
    val evenNumbers = numbers.filter { it % 2 == 0 }
    println("Nombres pairs: $evenNumbers") // Affiche: Nombres pairs: [2, 4, 6, 8, 10]

    val names = listOf("Alice", "Bob", "Charlie", "David", "Alex")

    // Filtrer les noms qui commencent par 'A'
    val namesStartingWithA = names.filter { it.startsWith("A") }
    println("Noms commençant par A: $namesStartingWithA") // Affiche: Noms commençant par A: [Alice, Alex]

    // Filtrer les noms courts (moins de 4 caractères)
    val shortNames = names.filter { it.length < 4 }
    println("Noms courts: $shortNames") // Affiche: Noms courts: [Bob]

    println("\nListe originale (inchangée): $numbers")
    println("Liste originale (inchangée): $names")
}

`filter` est l'outil idéal lorsque vous voulez extraire un sous-ensemble d'une collection basé sur une condition spécifique.

`map` : transformer chaque élément en une nouvelle valeur

L'opération `map` permet de créer une nouvelle collection en appliquant une transformation à chaque élément de la collection originale. Elle transforme chaque élément de type `T` en une nouvelle valeur de type `R` (qui peut être le même type ou un type différent).

Elle prend en argument une lambda qui définit cette transformation. Cette lambda est appelée pour chaque élément de la collection originale, et la valeur qu'elle retourne devient l'élément correspondant dans la nouvelle collection.

Comme `filter`, `map` ne modifie pas la collection originale et retourne une nouvelle instance (généralement une `List`) contenant les éléments transformés, dans le même ordre que les éléments originaux.

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)

    // Transformer chaque nombre en son carré
    // La lambda { it * it } retourne le carré de 'it'
    val squares = numbers.map { it * it }
    println("Carrés: $squares") // Affiche: Carrés: [1, 4, 9, 16, 25]

    val names = listOf("Alice", "Bob", "Charlie")

    // Transformer chaque nom en sa version majuscule
    val upperCaseNames = names.map { it.uppercase() }
    println("Noms en majuscules: $upperCaseNames") // Affiche: Noms en majuscules: [ALICE, BOB, CHARLIE]

    // Transformer chaque nom en sa longueur (changement de type: String -> Int)
    val nameLengths = names.map { it.length }
    println("Longueurs des noms: $nameLengths") // Affiche: Longueurs des noms: [5, 3, 7]

    // Transformer chaque nombre en une chaîne formatée
    val formattedNumbers = numbers.map { "Numéro [$it]" }
    println("Nombres formatés: $formattedNumbers") // Affiche: Nombres formatés: [Numéro [1], Numéro [2], Numéro [3], Numéro [4], Numéro [5]]

    println("\nListe originale (inchangée): $numbers")
    println("Liste originale (inchangée): $names")
}

`map` est essentielle lorsque vous voulez appliquer une même modification ou conversion à tous les éléments d'une collection pour obtenir un nouvel ensemble de données.

Combiner les opérations : le chaînage

La véritable élégance de ces opérations fonctionnelles réside dans leur capacité à être chaînées. Comme `filter` et `map` retournent de nouvelles collections, vous pouvez directement appliquer une autre opération sur le résultat de la précédente. Cela permet de construire des pipelines de traitement de données très lisibles et expressifs.

fun main() {
    val words = listOf("Kotlin", "is", "really", "fun", "and", "concise")

    // Objectif : Obtenir les longueurs des mots de 4 lettres ou plus, en majuscules.

    val result = words
        .filter { it.length >= 4 } // 1. Garder les mots longs: [Kotlin, really, concise]
        .map { it.uppercase() }    // 2. Mettre en majuscules: [KOTLIN, REALLY, CONCISE]
        .map { it.length }         // 3. Obtenir les longueurs: [6, 6, 7]
        // On pourrait aussi combiner les deux maps : .map { it.uppercase().length }

    println("Résultat final: $result") // Affiche: Résultat final: [6, 6, 7]

    // Autre exemple : Trouver les carrés des nombres pairs inférieurs à 10
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
    val squaresOfEvens = numbers
        .filter { it < 10 }      // [1, 2, 3, 4, 5, 6, 7, 8, 9]
        .filter { it % 2 == 0 } // [2, 4, 6, 8]
        .map { it * it }        // [4, 16, 36, 64]

    println("Carrés des pairs < 10: $squaresOfEvens") // Affiche: Carrés des pairs < 10: [4, 16, 36, 64]
}

Ce style de programmation, où l'on décrit une séquence de transformations et de filtrages, est souvent plus facile à lire et à maintenir que des boucles imbriquées avec des variables temporaires.

Récapitulatif : les trois piliers du traitement de collections

Retenez les rôles distincts de ces trois opérations fondamentales :

  • `forEach { action }` : Exécute une `action` (avec effets de bord) pour chaque élément. Ne retourne rien (`Unit`).
  • `filter { condition }` : Retourne une nouvelle collection contenant uniquement les éléments pour lesquels la `condition` (lambda retournant `Boolean`) est `true`.
  • `map { transformation }` : Retourne une nouvelle collection où chaque élément original a été transformé en une nouvelle valeur par la lambda de `transformation`.

Ces fonctions, utilisées avec des lambdas concises (souvent avec `it`), sont au coeur du traitement de données idiomatique en Kotlin et vous permettent d'écrire du code expressif, sûr (grâce à l'immutabilité des résultats de `filter` et `map`) et fonctionnel.