Introduction : pourquoi les generics ne font pas peur, mais sont pratiques
\n\nImaginez que vous écrivez une classe « Boîte ». Vous pouvez y mettre n'importe quoi : un livre, une pomme, un téléphone. Mais si vous la faites pour Object, à chaque fois que vous en retirez quelque chose, vous devrez faire as? Book et prier pour ne pas tomber sur une ClassCastException. Ça vous dit quelque chose ?
C'est là qu'interviennent les generics (génériques) — un puissant mécanisme de typage en Kotlin qui permet d'écrire du code flexible sans sacrifier la sécurité des types. Dans cet article, nous allons explorer les generics en Kotlin du simple au complexe : classes de base, fonctions, variance (in/out), types réifiés et projections stellaires. Vous apprendrez à rendre votre code plus propre et le compilateur votre meilleur allié.
\n\nCet article est préparé pour le portail éducatif pythonlib.ru — votre guide dans le monde des langages de programmation modernes.
\n\n1. Les bases : qu'est-ce que les generics en Kotlin ?
\n\nLes generics (types génériques) permettent de paramétrer des classes, des interfaces et des fonctions avec un type qui sera spécifié lors de l'utilisation. Cela évite la duplication de code et donne un contrôle sur les types au moment de la compilation.
\n\nRegardez cet exemple simple :
\n\nclass Box<T>(val item: T)\n\nfun main() {\n val bookBox = Box<String>("Kotlin in Action")\n val intBox = Box<Int>(42)\n // Le compilateur connaît le type !\n val title: String = bookBox.item\n}\n\nPoints clés :
\n- \n
<T>— paramètre de type (peut être n'importe quelle lettre, mais par convention T, R, E, K, V). \n - Le type est fixé lors de la création de l'objet — pas de casts à l'exécution. \n
- Les generics ne fonctionnent qu'au moment de la compilation (type erasure), mais Kotlin ajoute la « réification » (nous y reviendrons). \n
Fonctions génériques
\nLe typage en Kotlin fonctionne aussi pour les fonctions. Par exemple, une fonction qui retourne le premier élément d'une liste :
\n\nfun <T> firstItem(list: List<T>): T? = list.firstOrNull()\n\nfun main() {\n val numbers = listOf(1, 2, 3)\n val first: Int? = firstItem(numbers) // T = Int\n println(first) // 1\n}\n\nNotez que Kotlin peut déduire le type du contexte — il n'est pas nécessaire d'écrire firstItem<Int>(numbers).
2. Variance : in et out — comment ne pas casser la sécurité des types
\n\nLe sujet le plus difficile pour les débutants est la variance (variabilité). Elle répond à la question : est-ce que List<String> peut être un sous-type de List<Any> ?
En Java, cela se résout avec les wildcards (? extends / ? super). En Kotlin, avec out et in.
out (covariance) — lecture seule
\nL'annotation out signifie que le type générique ne peut être que « fourni » (retourné par les méthodes). Exemple — List<out T>.
interface Source<out T> {\n fun next(): T // OK : T en sortie\n // fun add(item: T) // Erreur ! T en entrée interdit\n}\n\nfun demo(source: Source<String>) {\n val anySource: Source<Any> = source // out permet cela\n}\n\nPourquoi est-ce sûr ? Si nous ne pouvons que lire des String depuis la source, alors on peut la considérer comme une source de Any.
in (contravariance) — écriture seule
\nin — l'inverse : le type ne peut être qu'en entrée. L'exemple classique est Comparable.
interface Comparable<in T> {\n operator fun compareTo(other: T): Int\n}\n\nfun demo(comp: Comparable<Number>) {\n val intComp: Comparable<Int> = comp // in permet cela\n}\n\nSi Comparable<Number> sait comparer n'importe quel nombre, alors il comparera certainement aussi Int.
in out (invariance) — par défaut
\nSi ni in ni out n'est spécifié, le type est inv