Contactez-nous

Collections mutables vs immutables (principe)

Comprenez la différence fondamentale entre les collections mutables (modifiables) et immutables (lecture seule) en Kotlin et pourquoi privilégier l'immutabilité.

Une distinction fondamentale dans la conception des collections Kotlin

L'une des décisions de conception clés dans la bibliothèque de collections de Kotlin est la séparation claire et explicite entre les collections immutables (lecture seule) et les collections mutables (modifiables). Cette distinction n'est pas juste une convention, elle est intégrée dans la hiérarchie des types (interfaces) et influence fortement la manière dont on écrit du code Kotlin idiomatique et sûr.

Comprendre ce principe est essentiel pour utiliser efficacement les collections en Kotlin et pour écrire du code robuste, prévisible, et plus facile à raisonner, notamment dans des contextes complexes comme le multi-threading. Par défaut, Kotlin encourage l'utilisation de l'immutabilité.

Collections immutables : la vue en lecture seule

Une collection immutable est une collection dont l'état (sa taille, les éléments qu'elle contient) ne peut pas être modifié après sa création. Pensez-y comme à un instantané, une photographie fixe de données.

Les interfaces de base que nous avons vues (`List`, `Set`, `Map`) représentent cette vue immutable. Elles ne fournissent que des opérations de lecture : accéder à des éléments (`get`, index `[]`), vérifier la taille (`size`), vérifier la présence d'éléments (`contains`, `containsKey`, `containsValue`), itérer sur les éléments, etc.

Elles ne proposent aucune méthode pour ajouter, supprimer ou modifier des éléments (pas de `add`, `remove`, `put`, `clear`...). Les fonctions standard comme `listOf()`, `setOf()`, `mapOf()` créent des instances de ces collections immutables.

fun main() {
    val immutableList = listOf("A", "B", "C")
    println(immutableList[0]) // OK: Lecture
    println(immutableList.size) // OK: Lecture

    // immutableList.add("D") // Erreur de compilation: Unresolved reference: add
    // La méthode 'add' n'existe tout simplement pas sur l'interface 'List'

    val immutableMap = mapOf("key1" to 1)
    println(immutableMap["key1"]) // OK: Lecture

    // immutableMap.put("key2", 2) // Erreur de compilation: Unresolved reference: put
    // La méthode 'put' n'existe pas sur l'interface 'Map'
}
Avantages de l'immutabilité :
  • Prévisibilité : Quand vous manipulez une collection immutable, vous avez la garantie absolue que son contenu ne changera pas de manière inattendue par une autre partie du code.
  • Sécurité (Thread Safety) : Les collections immutables sont intrinsèquement sûres à partager entre plusieurs threads sans nécessiter de synchronisation complexe, car il n'y a aucun risque de modification concurrente.
  • Raisonnement simplifié : Il est plus facile de comprendre le flux de données et l'état du programme lorsque les structures de données ne peuvent pas être modifiées subrepticement.

Collections mutables : la permission de modifier

Une collection mutable, en revanche, est une collection qui peut être modifiée après sa création. Vous pouvez ajouter de nouveaux éléments, en supprimer des existants, ou parfois même modifier les éléments en place (bien que cela dépende de la mutabilité des éléments eux-mêmes).

Kotlin fournit des interfaces spécifiques pour les collections mutables, qui héritent des interfaces immutables correspondantes et ajoutent les opérations de modification :

  • `MutableList` étend `List` et ajoute `add()`, `remove()`, `addAll()`, `removeAt()`, `clear()`, l'opérateur `set` (`[] =`), etc.
  • `MutableSet` étend `Set` et ajoute `add()`, `remove()`, `addAll()`, `removeAll()`, `clear()`, etc.
  • `MutableMap` étend `Map` et ajoute `put()`, `remove()`, `putAll()`, `clear()`, l'opérateur `set` (`[] =`), etc.

Pour créer des collections mutables, on utilise les fonctions `mutableListOf()`, `mutableSetOf()`, `mutableMapOf()`.

fun main() {
    val mutableList = mutableListOf("A", "B")
    println("Avant: $mutableList") // Affiche: Avant: [A, B]

    mutableList.add("C") // OK: Modification autorisée
    mutableList.remove("A") // OK: Modification autorisée
    mutableList[0] = "Z" // OK: Modification via l'opérateur set

    println("Après: $mutableList") // Affiche: Après: [Z, C]

    val mutableMap = mutableMapOf("key1" to 10)
    println("Map avant: $mutableMap") // Affiche: Map avant: {key1=10}

    mutableMap.put("key2", 20) // OK
    mutableMap["key1"] = 11 // OK (remplace la valeur existante)
    mutableMap.remove("keyNonExistante") // OK (ne fait rien si la clé n'existe pas)

    println("Map après: $mutableMap") // Affiche: Map après: {key1=11, key2=20}
}
Avantages de la mutabilité :
  • Flexibilité : Permet de construire ou de modifier une collection de manière dynamique au fil de l'exécution du programme.
  • Performance (parfois) : Pour certaines opérations d'ajout ou de suppression intensives, modifier une collection mutable en place peut être plus performant que de créer constamment de nouvelles collections immutables (bien que les implémentations immutables soient souvent optimisées).
Inconvénients potentiels de la mutabilité :
  • Complexité : Il est plus difficile de suivre l'état d'une collection mutable si elle peut être modifiée depuis plusieurs endroits du code.
  • Risques en multi-threading : Le partage de collections mutables entre threads sans synchronisation adéquate est une source majeure de bugs (race conditions, etc.).
  • Effets de bord : Une fonction qui modifie une collection mutable passée en argument peut avoir des effets de bord inattendus sur l'appelant.

La philosophie Kotlin : privilégier l'immutabilité

La bonne pratique fortement encouragée en Kotlin est de privilégier l'immutabilité par défaut. Utilisez les collections immutables (`List`, `Set`, `Map` créées avec `listOf`, `setOf`, `mapOf`) autant que possible.

N'utilisez les collections mutables (`MutableList`, `MutableSet`, `MutableMap`) que lorsque la logique de votre algorithme nécessite explicitement de modifier la collection après sa création. Par exemple, si vous construisez une liste de résultats au fur et à mesure dans une boucle, ou si vous maintenez un cache qui doit être mis à jour.

Cette approche rend votre code intrinsèquement plus sûr, plus facile à comprendre et à tester. Même lorsque vous utilisez une collection mutable localement dans une fonction, essayez de retourner ou d'exposer une version immutable si possible.

Relation entre les interfaces

Il est important de noter la relation d'héritage : `MutableList` est une `List`, `MutableSet` est un `Set`, et `MutableMap` est une `Map`. Cela signifie que vous pouvez toujours utiliser une collection mutable là où une collection immutable est attendue.

Par exemple, une fonction qui prend une `List` en paramètre peut accepter sans problème une `MutableList`. La fonction traitera alors la `MutableList` comme une simple `List` (en lecture seule) dans son propre scope, ce qui contribue à la sécurité.

fun printItemCount(items: List) { // Attend une List (immutable)
    println("Nombre d'éléments: ${items.size}")
    // items.add(...) // Erreur ici, car 'items' est vu comme une List
}

fun main() {
    val mutableData = mutableListOf(1, 2, 3)
    val immutableData = listOf("a", "b")

    printItemCount(mutableData) // OK: On passe une MutableList là où une List est attendue
    printItemCount(immutableData) // OK
}

Récapitulatif : le choix conscient

Le choix entre mutable et immutable n'est pas anodin en Kotlin :

  • Immutable (`List`, `Set`, `Map`) : Lecture seule, état fixe après création. Sûr, prévisible, idéal pour le partage. A privilégier par défaut. Créé avec `listOf()`, `setOf()`, `mapOf()`.
  • Mutable (`MutableList`, `MutableSet`, `MutableMap`) : Modifiable (ajout, suppression, etc.). Flexible mais demande plus de précautions. A utiliser lorsque la modification est nécessaire. Créé avec `mutableListOf()`, `mutableSetOf()`, `mutableMapOf()`.

En adoptant l'immutabilité par défaut, vous alignez votre code sur les bonnes pratiques Kotlin et bénéficiez d'une base plus solide pour construire des applications fiables.