Skip to content

Kotlin Aide-mémoire

Langage JVM moderne, concis et entièrement interopérable avec Java.

01

Bases

Variables et types

Préférez val (immuable) à var (mutable). Kotlin infère les types mais permet les annotations explicites. Utilisez 'is' pour les vérifications de type (smart-casts automatiquement). Tout est un objet—pas de primitives dans la syntaxe Kotlin.

kotlin
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List<Int> = listOf(1, 2, 3)
println(name::class)  // class kotlin.String
println(age is Int)   // true

Templates de chaînes

$var pour les variables simples, ${expr} pour les expressions. Les chaînes à triple guillemets préservent les nouvelles lignes—utilisez trimIndent() pour un formatage propre. Les templates de chaînes rendent la construction de chaînes concise et lisible sans format strings.

kotlin
val name = "Alice"
val age = 30
println("Name: $name, Age: $age")  // simple var
println("Length: ${name.length}")  // expression
println("Upper: ${name.uppercase()}")
val multiline = """
  Hello, $name
  Age: $age
""".trimIndent()

Sécurité null

La sécurité null de Kotlin : ? marque les types nullables, ?. appel sûr (retourne null si null), !! asserte non-null (lance NPE si null), ?: elvis (valeur par défaut si null). Cela élimine NullPointerException à la compilation—fonctionnalité centrale de Kotlin.

kotlin
var name: String? = null  // nullable type
println(name?.length)     // null (safe call)
name = "Alice"
println(name!!.length)    // 5 (non-null assertion)
val len: Int = name?.length ?: 0  // elvis operator
// Non-null types can never be null
val s: String = "hi"  // not nullable

Conversion de type

Kotlin nécessite une conversion de type explicite (pas de widening implicite comme Java). Utilisez toInt()/toDouble()/toString() pour la conversion. toIntOrNull() retourne null en cas d'échec—plus sûr que toInt() qui lance une exception. Gérez toujours null depuis l'analyse.

kotlin
val n: Int = 42
val d: Double = n.toDouble()
val s: String = n.toString()
val fromStr: Int? = "100".toIntOrNull()
val invalid: Int? = "abc".toIntOrNull()  // null
println(d, s, fromStr, invalid)

Vérifications de type et smart cast

is vérifie le type et smart-cast la variable dans cette branche—pas de cast explicite nécessaire. !is est la négation. Utilisez as pour les casts non sûrs (lance ClassCastException), as? pour les casts sûrs (retourne null en cas d'échec). Le smart cast est une commodité majeure de Kotlin.

kotlin
fun describe(x: Any): String =
    when (x) {
        is Int -> "Int: ${x + 1}"  // x smart-cast to Int
        is String -> "String of length ${x.length}"
        is List<*> -> "List with ${x.size} items"
        else -> "Unknown"
    }
println(describe(42))      // Int: 43
println(describe("hello")) // String of length 5
02

Chaînes de caractères

Méthodes courantes de chaînes

Les chaînes Kotlin ont de riches fonctions d'extension de la bibliothèque standard. La plupart retournent de nouvelles chaînes (immuables). Utilisez-les au lieu de boucles manuelles. Des fonctions comme uppercase() sont locale-aware—utilisez uppercase(Locale.ROOT) pour des résultats cohérents.

kotlin
val s = "Hello, World"
println(s.length)          // 12
println(s.uppercase())     // HELLO, WORLD
println(s.lowercase())     // hello, world
println(s.split(", "))     // [Hello, World]
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reversed())      // dlroW ,olleH
println(s.startsWith("Hello"))  // true

Chaînes multilignes et brutes

Les chaînes à triple guillemets sont brutes—pas de séquences d'échappement nécessaires (sauf triple double-guillemets). trimMargin('|') ne garde que le texte après | pour une indentation propre. trimIndent() supprime les espaces communs. Idéal pour SQL, JSON, HTML dans le code.

kotlin
val text = """
    |Hello,
    |World!
""".trimMargin()  // | marks line start
println(text)
val json = """{"name": "Alice", "age": 30}"""
println(json)  // no escape needed for quotes
// trimIndent() removes common leading whitespace

Construction de chaînes

joinToString est la manière idiomatique de joindre des collections avec séparateurs, préfixe, postfixe. buildString fournit une portée StringBuilder pour une construction concise. Évitez + répété dans les boucles—utilisez joinToString ou StringBuilder pour l'efficacité.

kotlin
val parts = listOf("apple", "banana", "cherry")
println(parts.joinToString(", "))  // apple, banana, cherry
println(parts.joinToString(prefix="[", postfix="]"))
val sb = StringBuilder()
for (p in parts) sb.append(p).append(" ")
println(sb.toString().trim())
// buildString for concise building
val result = buildString {
    parts.forEach { append(it).append(";") }
}

Chaîne vers nombre

toInt()/toDouble() lancent NumberFormatException sur entrée invalide. toIntOrNull() retourne null—associez avec ?: pour des valeurs par défaut sûres. toBooleanStrict() n'accepte que 'true'/'false'. Utilisez les variantes OrNull pour l'entrée utilisateur ou les données non fiables.

kotlin
val n = "42".toInt()           // 42 (throws if invalid)
val d = "3.14".toDouble()      // 3.14
val safe = "abc".toIntOrNull() // null
val withDefault = "x".toIntOrNull() ?: 0  // 0
val bool = "true".toBooleanStrict()  // true
println(n, d, safe, withDefault)

Regex

La classe Regex enveloppe le Pattern de Java. find retourne MatchResult? (première correspondance), findAll retourne toutes. matches vérifie la correspondance complète, containsMatchIn vérifie partielle. Utilisez les chaînes à triple guillemets pour les patterns afin d'éviter le double-escaping des backslashes.

kotlin
val email = Regex("[\w.]+@[\w]+\.[a-z]+")
val text = "Contact: [email protected]"
val match = email.find(text)
println(match?.value)  // [email protected]
println(email.matches("[email protected]"))  // true
val replaced = text.replace(Regex("\d+"), "#")
println(email.containsMatchIn(text))  // true
03

Structures de données

List

listOf crée des listes immuables, mutableListOf pour mutables. Les listes sont indexées à partir de zéro. Utilisez first()/last() pour les extrémités (lancent si vide), firstOrNull() pour la sécurité. contains vérifie l'appartenance. Préférez les listes immuables sauf si vous devez modifier.

kotlin
val nums = listOf(1, 2, 3)  // immutable
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable[0] = 0
println(nums.size)         // 3
println(nums.first())      // 1
println(nums.last())       // 3
println(nums.contains(2))  // true
println(nums.indexOf(2))   // 1

Map

mapOf crée des maps immuables, mutableMapOf pour mutables. 'to' infix crée des paires. [] access retourne V? nullable (null si manquant). Utilisez getOrDefault ou getValue (lance si manquant). Itérez avec déstructuration (k, v).

kotlin
val ages = mapOf("Alice" to 30, "Bob" to 25)
println(ages["Alice"])          // 30
println(ages.getOrDefault("Eve", 0))  // 0
println(ages.containsKey("Alice"))    // true
for ((k, v) in ages) println("$k: $v")
val mutable = mutableMapOf("a" to 1)
mutable["b"] = 2  // add/update

Set

setOf crée des sets immuables, mutableSetOf pour mutables. union/intersect/subtract retournent de nouveaux sets. Les sets imposent l'unicité—add retourne false si l'élément existe. Utilisez pour la déduplication et les opérations ensemblistes. LinkedHashSet préserve l'ordre d'insertion.

kotlin
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b)        // [1,2,3,4,5]
println(a intersect b)    // [3]
println(a subtract b)     // [1,2]
println(a.contains(2))    // true
val mutable = mutableSetOf(1, 2)
mutable.add(3)  // true if added

Array et tableaux primitifs

arrayOf crée Array<T> (boxé pour les primitives). Utilisez intArrayOf/doubleArrayOf pour les tableaux de primitives unboxed (meilleure performance). Array(size) { init } crée avec une fonction d'init. Les arrays sont mutables ; préférez List pour la plupart des usages.

kotlin
val arr = arrayOf(1, 2, 3)  // Array<Int>
arr[0] = 10
println(arr.size)  // 3
// Specialized arrays for primitives (no boxing)
val ints = intArrayOf(1, 2, 3)
val doubles = doubleArrayOf(1.0, 2.0)
// Array constructor
val squares = Array(5) { it * it }  // [0,1,4,9,16]
println(squares.toList())

Pair et Triple

Pair et Triple groupent 2-3 valeurs. 'to' infix crée des Pairs (courant pour les maps). Déstructurez avec val (a, b) = pair. Pour plus de 3 valeurs ou des champs nommés, utilisez les data classes—elles sont plus claires et plus maintenables.

kotlin
val pair = "Alice" to 30  // Pair<String, Int>
println(pair.first)   // Alice
println(pair.second)  // 30
val (name, age) = pair  // destructuring
val triple = Triple(1, "a", 2.0)
println(triple.first, triple.third)
// Useful for returning two values
fun minMax(list: List<Int>): Pair<Int, Int> =
    Pair(list.min(), list.max())
04

Flux de contrôle

If / Else (expression)

Dans Kotlin, if/else est une expression qui retourne une valeur. Cela remplace l'opérateur ternaire. Les deux branches doivent retourner des types compatibles. Utilisez pour une affectation conditionnelle concise. Comme instruction (sans else), retourne Unit.

kotlin
val score = 85
val grade = if (score >= 90) "A"
            else if (score >= 80) "B"
            else if (score >= 70) "C"
            else "F"
println(grade)  // B
// if returns a value—no ternary needed
// Both branches must have compatible types

When (switch)

when est le switch puissant de Kotlin—supporte les littéraux, ranges (in), négation (!in), valeurs multiples (virgule) et is-checks. Doit être exhaustif lorsqu'utilisé comme expression avec des types sealed. Sans sujet, c'est une chaîne if/else plus propre.

kotlin
val n = 2
val label = when (n) {
    0 -> "zero"
    1, 2, 3 -> "small"
    in 4..10 -> "medium"
    !in 1..100 -> "out of range"
    else -> "large"
}
println(label)  // small
// When without subject = multi-condition if
when {
    n > 0 -> println("positive")
    n < 0 -> println("negative")
    else -> println("zero")
}

Boucles for et ranges

.. est un range inclusif, until est exclusif, downTo compte à rebours, step définit l'incrément. withIndex() donne l'itération indexée. for-in fonctionne avec n'importe quel Iterable. Ces ranges sont concis et lisibles pour les boucles de comptage.

kotlin
for (i in 0 until 5) print(i)  // 01234 (excludes 5)
for (i in 1..5) print(i)      // 12345 (includes 5)
for (i in 5 downTo 1) print(i)  // 54321
for (i in 1..10 step 2) print(i)  // 13579
val list = listOf("a", "b", "c")
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

While et do-while

while vérifie avant, do-while vérifie après (s'exécute au moins une fois). Les deux nécessitent un état mutable. Préférez les boucles for ou les opérations fonctionnelles (map, filter) pour l'immutabilité. Utilisez while pour les boucles pilotées par condition où le compte n'est pas connu.

kotlin
var count = 0
while (count < 3) {
    println(count)
    count++
}
var j = 0
do {
    println(j)
    j++
} while (j < 3)
// do-while runs at least once
// Prefer for loops when possible

Break et continue (labels)

continue passe à l'itération suivante, break sort de la boucle. Les labels (@outer) permettent de sortir des boucles imbriquées—définissez avec label@ avant la boucle, break/continue avec @label. Utilisez avec parcimonie ; le refactoring en fonctions est souvent plus propre.

kotlin
for (i in 1..5) {
    if (i == 3) continue  // skip 3
    if (i == 5) break     // stop at 5
    println(i)  // 1, 2, 4
}
// Labeled breaks for nested loops
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i == 2 && j == 2) break@outer
        println("$i,$j")
    }
}
05

Fonctions et lambdas

Définition de fonction

Les fonctions utilisent fun name(params): ReturnType. Corps d'expression (= expr) pour les expressions uniques. Paramètres par défaut et arguments nommés supportés. Type de retour Unit = effet de bord uniquement. Les params par défaut réduisent le besoin de surcharge.

kotlin
fun add(a: Int, b: Int): Int = a + b  // expression body
fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg
fun log(msg: String): Unit = println(msg)  // Unit = void

Lambdas et ordre supérieur

Lambdas : { params -> body }. 'it' est le raccourci pour un paramètre unique. Passez les lambdas aux fonctions d'ordre supérieur comme map/filter/reduce. Syntaxe trailing lambda : si le dernier param est une fonction, il peut aller en dehors de (). C'est du Kotlin idiomatique.

kotlin
val square: (Int) -> Int = { x -> x * x }
println(square(5))  // 25
val nums = listOf(1, 2, 3)
println(nums.map { it * 2 })      // [2, 4, 6]
println(nums.filter { it > 1 })   // [2, 3]
println(nums.reduce { a, b -> a + b })  // 6
// 'it' is the implicit single parameter

Fonctions d'extension

Les fonctions d'extension ajoutent des méthodes aux classes existantes (même de Java) sans héritage ni modification. Elles sont du sucre syntaxique pour des fonctions statiques. 'this' fait référence au récepteur. Utilisez pour rendre les APIs fluentes. Résolu à la compilation (pas de dispatch dynamique).

kotlin
fun String.shout(): String = this.uppercase() + "!"
fun Int.isEven(): Boolean = this % 2 == 0
println("hello".shout())  // HELLO!
println(4.isEven())       // true
// Extensions don't modify the class—they're resolved statically
// Use to add utility methods to any type

Inline et infix

inline copie le corps de la fonction aux sites d'appel (élimine l'overhead des lambdas). Utilisez pour les fonctions d'ordre supérieur dans les chemins chauds. infix permet les appels de fonction sans point/parenthèses (a op b)—utilisez pour les DSL et opérations lisibles comme 'to', 'in', 'and'.

kotlin
inline fun measure(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println("Took ${System.currentTimeMillis() - start}ms")
}
measure { println("working...") }
// Infix functions for readable calls
infix fun Int.times(str: String): String = str.repeat(this)
println(3 times "ab")  // ababab

Vararg et spread

vararg accepte des arguments variables (collectés dans un tableau). * spread un tableau en vararg. Les params nommés peuvent suivre vararg. Utilisez pour des APIs flexibles comme listOf(), fonctions style printf. L'opérateur spread est l'équivalent Kotlin du spread JS.

kotlin
fun sum(vararg nums: Int): Int = nums.sum()
println(sum(1, 2, 3, 4))  // 10
// Spread operator to pass array
val arr = intArrayOf(1, 2, 3)
println(sum(*arr))  // 6
// Named/default params can follow vararg
fun greet(vararg names: String, greeting: String = "Hi") =
    names.joinToString { "$greeting, $it!" }
println(greet("A", "B"))
06

Classes et POO

Classe et constructeur

Le constructeur primaire est dans l'en-tête de classe. Les params val/var deviennent des propriétés (avec getters/setters). Sans val/var, ce sont juste des params de constructeur (non accessibles). Le bloc init s'exécute pendant la construction. Kotlin génère getters/setters automatiquement.

kotlin
class Person(val name: String, val age: Int) {
    fun greet(): String = "Hi, I'm $name"
    fun isAdult(): Boolean = age >= 18
}
val p = Person("Alice", 30)
println(p.greet())   // Hi, I'm Alice
println(p.name)      // Alice (val = property)
println(p.isAdult()) // true
// val/var in constructor = auto property

Data class

data class auto-génère equals, hashCode, toString, copy et componentN (pour la déstructuration). Utilisez pour les conteneurs de données. Doit avoir au moins un param val/var. copy() crée un clone modifié—excellent pour les mises à jour immuables. Déstructurez avec val (a, b) = point.

kotlin
data class Point(val x: Int, val y: Int)
val p1 = Point(3, 4)
val p2 = Point(3, 4)
println(p1 == p2)        // true (value equality)
println(p1.copy(x = 5))  // Point(x=5, y=4)
println(p1)              // Point(x=3, y=4)
// Auto: equals, hashCode, toString, copy, componentN
val (x, y) = p1  // destructuring via componentN()

Sealed class

Les sealed classes restreignent les sous-types au même fichier/module kotlin. Les expressions when sont exhaustives (le compilateur vérifie tous les cas). Utilisez pour les ADT (Algebraic Data Types) représentant des états finis. Combiné avec when, cela permet du pattern matching sûr et vérifié à la compilation.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
object Loading : Result()
fun handle(r: Result): String = when (r) {
    is Success -> "Got ${r.value}"
    is Failure -> "Error: ${r.error}"
    Loading -> "Loading..."
}
// when is exhaustive—compiler enforces all cases

Object et companion

object déclare un singleton (une instance, initialisé paresseusement). companion object dans une classe contient des membres 'statiques' (accessibles via ClassName.member). Utilisez object pour les singletons, companion pour les méthodes factory et constantes. Companion peut implémenter des interfaces.

kotlin
object Config {  // singleton
    val version = "1.0"
    fun load() = mapOf("key" to "value")
}
println(Config.version)  // 1.0
class User(val name: String) {
    companion object {
        fun create(name: String) = User(name)
        const val MAX = 100
    }
}
val u = User.create("Alice")  // like static method

Héritage et interface

Les classes sont final par défaut—utilisez 'open' pour permettre l'héritage. 'override' est requis. Les interfaces peuvent avoir des implémentations par défaut. Une classe étend une classe mais implémente plusieurs interfaces. Utilisez abstract pour l'implémentation partielle. Préférez la composition à l'héritage.

kotlin
open class Animal(val name: String) {
    open fun speak() = "..."
}
class Dog(name: String) : Animal(name) {
    override fun speak() = "Woof"
}
interface Greetable {
    val name: String
    fun greet(): String  // can have default impl
}
class Person(override val name: String) : Greetable {
    override fun greet() = "Hi, $name"
}
07

Sécurité null et fonctions de portée

Appels sûrs et elvis

?. appel sûr (retourne null si le récepteur est null), ?: elvis (fournit une valeur par défaut). Chaînez ?. pour l'accès profond (user?.address?.city). Utilisez ?: return/throw pour les sorties anticipées. Cela rend la gestion null concise et sûre—pas de vérifications null explicites nécessaires.

kotlin
var name: String? = "Alice"
println(name?.length)      // 5
name = null
println(name?.length)      // null
println(name?.length ?: 0) // 0 (elvis: default if null)
println(name?.length ?: return)  // early return
// Safe call chain
val city: String? = user?.address?.city

let (vérification null)

let exécute un bloc uniquement si la valeur est non-null. 'it' est la valeur non-null dans le bloc. Utilisez pour les opérations gardées par null au lieu des vérifications if-null. Pattern courant : value?.let { ... } pour un traitement sûr. Retourne le résultat du bloc.

kotlin
var name: String? = "Alice"
name?.let {
    println("Name is $it")  // only runs if not null
    println("Length: ${it.length}")  // it is non-null
}
// Common for null-guarded blocks
val result = name?.let {
    process(it)  // it: String (non-null)
} ?: "default"

apply et also

apply configure un objet (retourne l'objet, récepteur 'this')—idéal pour les builders. also effectue des effets de bord (retourne l'objet, param 'it')—bon pour le logging/debugging dans les chaînes. Les deux retournent l'objet original, permettant des chaînes fluentes.

kotlin
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)  // 'this' = the list
    println("Built $size items")
}
val config = Config().also {
    it.timeout = 30  // 'it' = config
    println("Configured")
}
// apply: configure and return object (this)
// also: side effects, return object (it)

run et with

run exécute un bloc sur un objet (retourne le résultat du bloc, récepteur 'this')—utilisez pour transformer un objet. with est comme run mais prend l'objet comme paramètre (pas chaînable sur nullables). Utilisez pour grouper des opérations sur le même objet.

kotlin
val result = "Hello".run {
    length  // 'this' = string, returns last expr
}  // 5
val r2 = with(StringBuilder()) {
    append("a"); append("b")
    toString()  // returns this
}
// run: object.run { } - returns block result
// with: with(obj) { } - returns block result (not chainable)

TakeIf et TakeUnless

takeIf retourne l'objet si le prédicat est vrai, null sinon. takeUnless est l'opposé. Utilisez pour le filtrage conditionnel dans les chaînes—évite les vérifications if séparées. Combinez avec ?: pour les valeurs par défaut. Élégant pour les pipelines de validation.

kotlin
val age = 25
val valid = age.takeIf { it >= 18 }  // 25 (if true)
val invalid = age.takeIf { it < 18 }  // null (if false)
val adult = age.takeUnless { it < 18 }  // 25
// Useful for filtering in chains
val email = getUser()?.email?.takeIf { it.contains("@") }
println(valid, invalid, adult)
08

Collections et fonctionnel

Map / Filter / Fold

map transforme, filter sélectionne, reduce/fold agrège. 'it' est l'élément implicite. fold prend une graine ; reduce non (lance si vide). Ce sont le cœur du traitement fonctionnel des collections—utilisez au lieu des boucles pour la clarté.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
println(nums.map { it * 2 })          // [2,4,6,8,10]
println(nums.filter { it % 2 == 0 })  // [2,4]
println(nums.reduce { a, b -> a + b }) // 15
println(nums.fold(0) { a, b -> a + b }) // 15
println(nums.sum())                   // 15
println(nums.joinToString(", "))      // 1, 2, 3, 4, 5

FlatMap et GroupBy

flatten supprime un niveau d'imbrication. flatMap map et aplatit en une étape—essentiel pour les transformations imbriquées. groupBy partitionne par clé dans un Map. Ce sont puissants pour le traitement de données et les pipelines d'analyse.

kotlin
val nested = listOf(listOf(1, 2), listOf(3, 4))
println(nested.flatten())         // [1,2,3,4]
println(nested.flatMap { it.map { n -> n * 2 } })  // [2,4,6,8]
val words = listOf("apple", "bat", "ant")
val byFirst = words.groupBy { it.first() }
// {a=[apple, ant], b=[bat]}
println(byFirst)

Tri

sorted/sortedDescending trient naturellement. sortedBy/sortedByDescending trient par un sélecteur de clé. Ils retournent de nouvelles listes (immuables). Pour les listes mutables, utilisez sort/sortBy (sur place). Utilisez les sélecteurs de clé pour trier par un champ spécifique.

kotlin
val nums = listOf(3, 1, 4, 1, 5)
println(nums.sorted())           // [1,1,3,4,5]
println(nums.sortedDescending()) // [5,4,3,1,1]
val people = listOf("Alice" to 30, "Bob" to 25)
val byAge = people.sortedBy { it.second }  // by age
println(byAge)
val byNameDesc = people.sortedByDescending { it.first }
println(byNameDesc)

Sequences (paresseux)

Les sequences sont paresseuses—les opérations sont différées jusqu'à une opération terminale (toList, sum, count). Évite les collections intermédiaires pour une meilleure performance sur les grandes données. Utilisez asSequence() pour les pipelines multi-étapes sur de grandes collections. Comme Java Streams.

kotlin
val nums = (1..1000000).toList()
// Eager: creates intermediate lists
val eager = nums.filter { it % 2 == 0 }.map { it * 2 }.take(5)
// Lazy: no intermediate collections
val lazy = nums.asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .toList()  // forces evaluation
println(lazy)  // [4, 8, 12, 16, 20]

Partition et Chunked

partition sépare en deux listes par un prédicat (retourne Pair). chunked divise en listes de taille fixe. windowed crée des fenêtres glissantes. Utiles pour le batching, la pagination et les algorithmes à fenêtre glissante. Tous retournent de nouvelles collections.

kotlin
val nums = listOf(1, 2, 3, 4, 5)
val (evens, odds) = nums.partition { it % 2 == 0 }
println(evens)  // [2, 4]
println(odds)   // [1, 3, 5]
val chunked = nums.chunked(2)
println(chunked)  // [[1, 2], [3, 4], [5]]
val windowed = nums.windowed(3)
println(windowed)  // [[1,2,3], [2,3,4], [3,4,5]]
09

Coroutines et async

Launch (fire and forget)

launch démarre une coroutine qui ne retourne pas de résultat (fire-and-forget). delay est non-bloquant (contrairement à Thread.sleep). runBlocking fait le pont sync/async (utilisez dans main/tests). Les coroutines sont légères—des milliers peuvent s'exécuter sur quelques threads.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch {
        delay(1000)
        println("World!")  // after 1s
    }
    println("Hello")  // immediately
    // launch doesn't return a result
    // Use for side-effect coroutines
}
// Output: Hello, then World! after 1s

Async et await

async démarre une coroutine qui retourne un Deferred<T>. await() suspend jusqu'à ce que le résultat soit prêt. Démarrez plusieurs async avant d'await pour le parallélisme. Utilisez pour les calculs concurrents qui produisent des résultats. Comme Promise/Future dans d'autres langages.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val deferred = async {
        delay(1000)
        42  // return value
    }
    val result = deferred.await()
    println(result)  // 42
    // Parallel execution
    val a = async { computeA() }
    val b = async { computeB() }
    println(a.await() + b.await())  // concurrent
}

Fonctions suspend

Les fonctions suspend peuvent se mettre en pause et reprendre sans bloquer les threads. Elles ne peuvent être appelées que depuis des coroutines ou d'autres fonctions suspend. coroutineScope fournit une portée structurée (attend tous les enfants). Utilisez pour les APIs async—rend le code async synchrone en apparence.

kotlin
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): String {
    delay(500)  // simulates network
    return "User $id"
}
suspend fun fetchAll(): List<String> = coroutineScope {
    val a = async { fetchUser(1) }
    val b = async { fetchUser(2) }
    listOf(a.await(), b.await())
}
// suspend functions can only be called from coroutines

Flow (flux froid)

Flow est le flux async froid de Kotlin (comme RxJava Observable). Les valeurs sont produites à la collection. Utilisez les opérateurs map/filter/reduce. emit produit, collect consomme. Idéal pour le streaming de données, événements ou APIs paginées. Les flux chauds utilisent SharedFlow/StateFlow.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)  // produce value
    }
}
fun main() = runBlocking {
    numbers().collect { println(it) }  // 1,2,3,4,5
    // Transform
    numbers().map { it * 2 }.filter { it > 4 }.collect {
        println(it)  // 6, 8, 10
    }
}

Dispatchers et contexte

Les Dispatchers choisissent le thread pool : Main pour l'UI, IO pour réseau/fichier (grand pool), Default pour le travail CPU (nombre de cœurs). withContext change de contexte dans une coroutine. Utilisez le bon dispatcher pour éviter de bloquer l'UI ou d'affamer les thread pools.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    launch(Dispatchers.Main) { /* UI thread */ }
    launch(Dispatchers.IO) {
        // Network/file I/O (thread pool)
        val data = fetchData()
    }
    launch(Dispatchers.Default) {
        // CPU-intensive work
        val result = heavyCompute()
    }
    // withContext switches dispatcher
    val data = withContext(Dispatchers.IO) { readFile() }
}
10

Gestion d'erreurs et I/O

Try / Catch / Finally

try/catch/finally est comme Java mais try est une expression retournant une valeur. Pas d'exceptions checked—toutes sont unchecked. Attrapez des exceptions spécifiques pour un traitement ciblé. Utilisez finally pour le nettoyage. Préférez Result ou les retours nullables pour les échecs attendus.

kotlin
val result = try {
    "abc".toInt()
} catch (e: NumberFormatException) {
    0  // fallback
} finally {
    println("cleanup")
}
println(result)  // 0
// try is an expression—returns a value
// Kotlin doesn't have checked exceptions

Type Result

Result<T> enveloppe succès ou échec (comme Try dans Scala). runCatching convertit les exceptions en Result. getOrNull/getOrElse pour un accès sûr. onSuccess/onFailure pour les callbacks. Utilisez pour les erreurs attendues au lieu des exceptions—gestion fonctionnelle des erreurs plus propre.

kotlin
fun parse(s: String): Result<Int> =
    runCatching { s.toInt() }
val r = parse("42")
println(r.getOrNull())    // 42
println(r.getOrElse { 0 }) // 42
r.onSuccess { println("OK: $it") }
    .onFailure { println("Err: ${it.message}") }
val r2 = parse("abc")
println(r2.getOrNull())   // null

Exceptions personnalisées

Les exceptions personnalisées étendent Exception (ou une sous-classe). Passez un message pour le débogage. Attrapez les exceptions spécifiques avant les génériques (l'ordre compte). Utilisez les exceptions pour les cas vraiment exceptionnels ; pour les échecs attendus, préférez Result ou les retours nullables.

kotlin
class InvalidAgeException(message: String) : Exception(message)
fun setAge(age: Int) {
    if (age < 0) throw InvalidAgeException("Age cannot be negative: $age")
}
try {
    setAge(-5)
} catch (e: InvalidAgeException) {
    println("Caught: ${e.message}")
} catch (e: Exception) {
    println("Other: ${e.message}")
}

I/O fichier

Kotlin utilise le File de Java avec des fonctions d'extension. writeText/readText pour les cas simples. useLines diffuse les lignes (ferme automatiquement, économe en mémoire). Pour les gros fichiers, utilisez bufferedReader(). Fermez toujours les ressources—utilisez le bloc use { } pour la fermeture automatique.

kotlin
import java.io.File
// Write
File("test.txt").writeText("Hello, File!")
// Read
val content = File("test.txt").readText()
println(content)  // Hello, File!
// Append
File("log.txt").appendText("new line\n")
// Line by line
File("test.txt").useLines { lines ->
    lines.forEach { println(it) }
}

JSON (kotlinx.serialization)

kotlinx.serialization est la bibliothèque JSON officielle de Kotlin—sûre à la compilation, pas de réflexion. Annototez les data classes avec @Serializable. encodeToString/decodeFromString pour JSON. Utilisez @SerialName pour les noms de clés personnalisés, @Optional pour les valeurs par défaut. Alternative : Gson/Moshi.

kotlin
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val json = Json.encodeToString(user)
println(json)  // {"name":"Alice","age":30}
val decoded = Json.decodeFromString<User>(json)
println(decoded.name)  // Alice
// Requires @Serializable annotation
11

Coroutines en profondeur

Concurrence structurée

La concurrence structurée lie les coroutines enfants à une portée parent—le parent ne se termine pas tant que tous les enfants ne sont pas finis, et un échec d'enfant annule les frères. Préférez coroutineScope à GlobalScope. Cela prévient les fuites de coroutine et rend l'annulation prévisible.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // coroutineScope waits for all children
    coroutineScope {
        launch { delay(1000); println("A") }
        launch { delay(500); println("B") }
    }
    println("Done")  // after both complete
    // If a child fails, parent cancels siblings
    // Never use GlobalScope unless necessary
}

Annulation et annulation coopérative

L'annulation est coopérative—les coroutines doivent la vérifier aux points de suspension (delay, yield, await). Les fonctions suspend annulables lancent CancellationException. Utilisez ensureActive() ou isActive pour vérifier. Les boucles CPU non-suspendantes ne répondront pas à l'annulation sauf si vous appelez yield() ou vérifiez isActive.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val job = launch {
        repeat(10) { i ->
            delay(300)
            yield()  // explicit suspension point
            println("Working $i")
        }
    }
    delay(700)
    job.cancelAndJoin()  // cancels and waits
    println("Cancelled")
}
// delay/yield check for cancellation
// isActive checks cancellation state

Gestion des exceptions

Les exceptions non gérées dans launch se propagent au parent (annulant les frères) sauf si un CoroutineExceptionHandler est installé. async diffère les exceptions à await(). Utilisez try/catch autour de await. Le handler fonctionne uniquement pour les exceptions non attrapées dans launch—utilisez SupervisorJob pour isoler les échecs.

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e ->
        println("Caught: $e")
    }
    val job = launch(handler) {
        throw RuntimeException("Boom!")
    }
    job.join()
    // async exceptions surface on await()
    val deferred = async { throw IllegalStateException() }
    try { deferred.await() } catch (e: Exception) {
        println("Async failed: $e")
    }
}

Supervision (supervisorScope)

supervisorScope (et SupervisorJob) crée une portée où l'échec d'un enfant n'annule PAS ses frères—chaque enfant échoue indépendamment. Utilisez-le pour des opérations indépendantes (ex. multiples appels API indépendants). Le coroutineScope régulier annule les frères au premier échec (fail-fast).

kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
    // supervisorScope: child failures don't cancel siblings
    supervisorScope {
        launch { delay(100); throw RuntimeException("A fails") }
        launch { delay(200); println("B still runs") }
    }
    // vs coroutineScope: A's failure cancels B
    // Use SupervisorJob for long-lived services
    val scope = CoroutineScope(SupervisorJob())
}

Channels (style CSP)

Les Channels passent des valeurs entre coroutines (comme les channels Go). Rendezvous (capacité 0) synchronise émetteur et récepteur ; buffered permet la file d'attente. send suspend quand plein, receive quand vide. Fermez toujours les producteurs avec close(). Pour le fan-out, utilisez BroadcastChannel ou SharedFlow. Préférez Flow pour la plupart des besoins de streaming.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
    val channel = Channel<Int>(3)  // buffered capacity 3
    launch {
        for (i in 1..5) {
            channel.send(i)
            println("Sent $i")
        }
        channel.close()
    }
    // receive() blocks until available
    for (x in channel) println("Got $x")
    // Produce-consume pattern
    // Rendezvous (cap 0) syncs sender/receiver
}
12

Flow en profondeur

Opérateurs Flow

Les opérateurs Flow sont froids—ils s'exécutent uniquement lorsqu'ils sont collectés. map/filter/take sont comme Stream/Sequence. transform est le plus flexible (peut émettre plusieurs valeurs par entrée). Les variantes flatMap gèrent les flows imbriqués : concat (séquentiel), merge (concurrent), latest (annule le précédent). Choisissez selon les besoins d'ordonnancement/concurrence.

kotlin
import kotlinx.coroutines.flow.*
fun nums() = flow {
    for (i in 1..5) emit(i)
}
suspend fun main() {
    nums().map { it * it }           // 1,4,9,16,25
        .filter { it > 5 }          // 9,16,25
        .take(2)                    // 9,16
        .collect { println(it) }
    // transform: emit multiple values
    nums().transform { x ->
        emit(x)
        emit(x * 10)
    }.collect { println(it) }
    // flatMapConcat / flatMapMerge / flatMapLatest
}

Buffer et concurrence

buffer découple le producteur et le consommateur avec une file à capacité fixe—utile lorsque les vitesses de production et consommation diffèrent. conflate ne garde que la dernière valeur (drop les intermédiaires) pour les mises à jour UI/état. collectLatest annule le collecteur précédent lorsqu'une nouvelle valeur arrive—idéal pour search-as-you-type.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun events() = flow {
    for (i in 1..3) {
        delay(100); emit(i)
    }
}
fun main() = runBlocking {
    // Without buffer: producer & consumer alternate (slow)
    events().buffer()  // decouple producer/consumer
        .collect { delay(200); println(it) }
    // conflate: drop intermediate values
    events().conflate().collect { println(it) }
    // collectLatest: cancel previous collection
    events().collectLatest { delay(200); println(it) }
}

StateFlow et SharedFlow (flux chauds)

StateFlow est un flux chaud tenant une seule valeur à jour—utilisez-le pour l'état UI (remplace LiveData). Il a toujours une valeur et conflate. SharedFlow est un flux de diffusion chaud (multiple collecteurs, pas de valeur initiale requise)—utilisez-le pour les événements. SharedFlow avec replay=1 se comporte comme un bus d'événements en cache.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    // StateFlow: holds one value, stateful
    val state = MutableStateFlow(0)
    launch {
        state.collect { println("State: $it") }
    }
    state.value = 1  // update synchronously
    state.value = 2
    // SharedFlow: broadcasts to multiple collectors
    val events = MutableSharedFlow<String>()
    launch { events.collect { println("A: $it") } }
    launch { events.collect { println("B: $it") } }
    events.emit("Hello")  // both receive
}

Gestion des exceptions Flow

catch gère les exceptions en amont et peut émettre des valeurs de repli. Il n'attrape que les exceptions émises avant lui dans la chaîne. retry/retryWhen recollectent le flux en cas d'échec (utile pour les appels réseau). Pour les exceptions en aval, enveloppez collect dans un try/catch. N'attrapez jamais d'exceptions dans le constructeur de flux—laissez-les se propager.

kotlin
import kotlinx.coroutines.flow.*
fun numbers() = flow {
    emit(1)
    throw RuntimeException("fail")
    emit(2)  // never reached
}
suspend fun main() {
    // catch operator (upstream only)
    numbers().catch { e -> emit(-1) }
        .collect { println(it) }  // 1, -1
    // retry on failure
    numbers().retry(3) { e ->
        println("Retry: $e"); true
    }.collect { println(it) }
    // retryWhen for custom logic
}

flowOn & Contexte

flowOn change le répartiteur pour le flux en amont (producteur + opérateurs au-dessus de lui). C'est essentiel lorsque le flux fait des E/S bloquantes—enveloppez-le avec flowOn(Dispatchers.IO). L'aval (collect) s'exécute sur le contexte de l'appelant. Plusieurs appels flowOn créent des contextes séparés pour chaque segment.

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun diskFlow() = flow {
    for (i in 1..3) {
        Thread.sleep(100)  // blocking I/O
        emit(i)
    }
}
suspend fun main() {
    // flowOn changes upstream context
    diskFlow().flowOn(Dispatchers.IO)
        .collect { println(it) }
    // Without flowOn, runs on collector's dispatcher
    // flowOn applies to all operators above it
    diskFlow().map { it * 2 }
        .flowOn(Dispatchers.IO)
        .filter { it > 2 }
        .collect { println(it) }
}
13

Classes sealed & ADT

Bases des classes sealed

Les classes sealed restreignent les sous-classes à un ensemble connu (même fichier/module). Le compilateur connaît tous les types possibles, permettant des expressions when exhaustives sans else. Idéal pour modéliser des états finis (Result, UiState, réponses réseau). Combinées avec les data classes, elles forment les Types de Données Algébriques.

kotlin
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
fun handle(r: Result<Int>) = when (r) {
    is Result.Success -> println("Got ${r.value}")
    is Result.Failure -> println("Error: ${r.error}")
    Result.Loading -> println("Loading...")
}
// All subclasses defined in same file/module

Interfaces sealed (Kotlin 1.5+)

Les interfaces sealed (Kotlin 1.5+) étendent sealed aux interfaces, permettant à une classe d'implémenter plusieurs types sealed—plus flexible que les classes sealed. La restriction même fichier/même module s'applique. Utile pour la modélisation de domaine où un type appartient à plusieurs catégories.

kotlin
sealed interface Shape {
    fun area(): Double
}
data class Circle(val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}
data class Square(val side: Double) : Shape {
    override fun area() = side * side
}
// Sealed interfaces allow multiple inheritance
sealed interface Clickable { fun click() }
class Button : Shape, Clickable {
    override fun area() = 0.0
    override fun click() = println("Clicked")
}

When exhaustif

When utilisé comme expression (et non instruction), le compilateur exige tous les cas sealed—pas de else nécessaire. Ajouter une nouvelle sous-classe produit des erreurs de compilation sur les sites when non gérés, rendant le refactoring sûr. C'est la fonctionnalité clé des classes sealed pour les machines à états et le rendu UI.

kotlin
sealed class NetworkState {
    object Loading : NetworkState()
    data class Success(val data: String) : NetworkState()
    data class Error(val message: String) : NetworkState()
}
fun render(state: NetworkState): String = when (state) {
    NetworkState.Loading -> "Spinner"
    is NetworkState.Success -> "Data: ${state.data}"
    is NetworkState.Error -> "Error: ${state.message}"
    // No else needed—compiler enforces all cases
}
// Adding a new subclass causes compile error
// until you handle it

Modélisation ADT (Result/Either)

Les classes sealed modélisent des types somme (Either/Result) pour la programmation orientée railway. fold gère les deux branches. Contrairement aux exceptions, les erreurs sont explicites dans la signature de type. Arrow-kt fournit un Either/Validated plus riche. Utilisez-les pour une gestion d'erreurs prévisible et composable sans try/catch.

kotlin
sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}
fun <L, R> Either<L, R>.fold(
    ifLeft: (L) -> Unit,
    ifRight: (R) -> Unit
) = when (this) {
    is Either.Left -> ifLeft(value)
    is Either.Right -> ifRight(value)
}
fun divide(a: Int, b: Int): Either<String, Int> =
    if (b == 0) Either.Left("Divide by zero")
    else Either.Right(a / b)

Classes sealed avec récursion

Les classes sealed peuvent être récursives, modélisant des structures arboresques (AST, JSON, expressions). Le pattern matching avec when + récursion les évalue élégamment. C'est l'usage classique des ADT fonctionnels—type-safe, exhaustif et adapté au refactoring. Utilisé dans les compilateurs, parseurs et DSL de configuration.

kotlin
sealed class Expr {
    data class Num(val value: Int) : Expr()
    data class Add(val left: Expr, val right: Expr) : Expr()
    data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Add -> eval(e.left) + eval(e.right)
    is Expr.Mul -> eval(e.left) * eval(e.right)
}
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
fun main() = println(eval(expr))  // 14
14

Propriétés déléguées

Délégué lazy

lazy diffère l'initialisation jusqu'au premier accès et met en cache le résultat. Par défaut thread-safe (double-checked locking). Utilisez-le pour les ressources coûteuses (configs, connexions DB, singletons) qui pourraient ne pas être nécessaires. Passez le mode NONE pour les contextes mono-thread afin d'éviter le coût de synchronisation.

kotlin
val heavyConfig: Config by lazy {
    println("Initializing...")
    loadConfigFromFile()  // runs once, first access
}
fun main() {
    println("Before access")
    println(heavyConfig)  // initializes here
    println(heavyConfig)  // cached, no re-init
}
// lazy is thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// Use LazyThreadSafetyMode.NONE for single-threaded

observable & vetoable

observable déclenche un callback après chaque changement (journalisation, effets de bord). vetoable peut rejeter les changements en retournant false (validation). Les deux prennent une valeur initiale et un lambda. Utilisez-les pour l'état réactif, la validation ou le déclenchement de mises à jour UI. Pour des scénarios plus complexes, utilisez des délégués personnalisés.

kotlin
import kotlin.properties.Delegates
var count: Int by Delegates.observable(0) { _, old, new ->
    println("Changed: $old -> $new")
}
var age: Int by Delegates.vetoable(0) { _, old, new ->
    new >= 0  // reject negative values
}
fun main() {
    count = 1   // prints "Changed: 0 -> 1"
    count = 2   // prints "Changed: 1 -> 2"
    age = 25    // accepted
    age = -5    // rejected, stays 25
}

Délégué de propriété personnalisé

Les délégués personnalisés implémentent getValue (et setValue pour les vars) via ReadOnlyProperty/ReadWriteProperty. Ils encapsulent un comportement de propriété réutilisable (validation, formatage, cache, mapping DB). thisRef est le propriétaire, prop est les métadonnées de la propriété. Puissant pour les frameworks ORM/sérialisation.

kotlin
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString : ReadWriteProperty<Any?, String> {
    private var value: String = ""
    override fun getValue(thisRef: Any?, prop: KProperty<*>) = value
    override fun setValue(thisRef: Any?, prop: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}
class User {
    var name: String by TrimmedString()
}
fun main() {
    val u = User()
    u.name = "  Alice  "
    println(u.name)  // "Alice"
}

Propriétés basées sur Map

Déléguer à une Map vous permet de lier des propriétés à des clés dynamiques—utile pour analyser JSON, configs ou lignes ORM sans boilerplate. Le nom de la propriété doit correspondre à la clé de la map. Utilisez MutableMap pour les propriétés inscriptibles. C'est ainsi que fonctionnent kotlinx.serialization et de nombreux ORM en interne.

kotlin
class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
    val email: String? by map
}
fun main() {
    val u = User(mapOf(
        "name" to "Alice",
        "age" to 30,
        "email" to "[email protected]"
    ))
    println(u.name)  // Alice
}
// Mutable version: MutableMap
class MutableUser(map: MutableMap<String, Any?>) {
    var name: String by map
}

notNull & Délégué Singleton

notNull() est comme lateinit mais pour tout type (y compris les primitifs) et fonctionne avec val/var. Il lève une exception si accédé avant initialisation. Pour les singletons, préférez object (eager) ou by lazy (lazy). lateinit est pour les vars dans les classes uniquement ; le délégué notNull est plus flexible mais a un léger coût.

kotlin
import kotlin.properties.Delegates
class Service {
    // Late-init for non-null, set once
    var config: String by Delegates.notNull()
    // Throws before initialization
}
// Singleton via object (no delegate needed)
object Database {
    val connection = connect()
}
// Or lazy singleton
val db: Database by lazy { Database() }
fun main() {
    val s = Service()
    // println(s.config)  // IllegalStateException
    s.config = "prod"
    println(s.config)  // prod
}
15

Construction de DSL

Builders type-safe

Les builders type-safe utilisent des types de fonction avec receveur (lambda avec receveur) pour créer des DSL imbriqués. Le lambda s'exécute dans le contexte du receveur (this), vous appelez donc ses méthodes directement. C'est ainsi que fonctionnent le DSL HTML de Kotlin, les scripts de build Gradle et kotlinx.html—déclaratif et vérifié à la compilation.

kotlin
class Table {
    private val rows = mutableListOf<Row>()
    fun row(init: Row.() -> Unit) {
        val r = Row(); r.init(); rows.add(r)
    }
    fun build() = rows
}
class Row {
    private val cells = mutableListOf<String>()
    fun cell(text: String) { cells.add(text) }
    fun build() = cells
}
fun table(init: Table.() -> Unit) = Table().apply(init).build()
val t = table {
    row { cell("A"); cell("B") }
    row { cell("C"); cell("D") }
}

@DslMarker (Contrôle de portée)

@DslMarker empêche la fuite implicite du receveur—dans un bloc DSL imbriqué, vous ne pouvez appeler que les méthodes du receveur le plus interne. Sans lui, Kotlin permettrait d'appeler les méthodes des receveurs externes, conduisant à des DSL confus/buggés. Annotete toutes les classes DSL avec la même annotation marqueur.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(b: Body.() -> Unit) { /*...*/ } }
@HtmlDsl
class Body { fun p(text: String) { /*...*/ } }
fun html(init: HTML.() -> Unit) = HTML().apply(init)
// Without @DslMarker, inner 'this' could call outer methods
html {
    body {
        // p() is unambiguous—Body's method
        // body() would be an error (not Body's method)
        p("Hello")
    }
}

Fonctions infix

Les fonctions infix permettent une syntaxe en langage naturel (a to b, 1 until 10). Elles doivent être des fonctions membre ou d'extension avec un seul paramètre. Utilisées massivement dans les DSL, frameworks de test et bibliothèques mathématiques. Exemples intégrés : to (Pair), until/step (intervalles) et opérations de collection.

kotlin
infix fun Int.times(str: String) = str.repeat(this)
fun main() {
    println(3 times "ab")  // "ababab"
    // Equivalent to: 3.times("ab")
    // No dot, no parentheses—reads like natural language
}
// Common in testing
infix fun <T> T.shouldEqual(expected: T) =
    assert(this == expected)
fun test() { 5 shouldEqual 5 }
// Also: to, until, step, in, etc.

Surcharge d'opérateurs

La surcharge d'opérateurs permet aux objets d'utiliser +, -, *, [], (), etc. avec une syntaxe naturelle. Marquez les fonctions avec operator. Surchargz de manière sensée—les types mathématiques (Vec, Matrix, Money) en bénéficient ; la surcharge arbitraire nuit à la lisibilité. get/set permettent l'indexation ; invoke rend les objets appelables. Les opérateurs d'intervalle (rangeTo, contains) alimentent les boucles for.

kotlin
data class Vec(val x: Int, val y: Int) {
    operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
    operator fun minus(o: Vec) = Vec(x - o.x, y - o.y)
    operator fun times(s: Int) = Vec(x * s, y * s)
    operator fun unaryMinus() = Vec(-x, -y)
    operator fun get(i: Int) = when (i) { 0 -> x; 1 -> y; else -> throw IndexOutOfBoundsException() }
}
fun main() {
    val a = Vec(1, 2)
    val b = Vec(3, 4)
    println(a + b)        // Vec(x=4, y=6)
    println(a * 2)        // Vec(x=2, y=4)
    println(-a)           // Vec(x=-1, y=-2)
    println(a[0])         // 1
}

Types de fonction avec receveur

Les types de fonction avec receveur (A.(B) -> C) permettent aux lambdas d'accéder au receveur comme 'this'. C'est le fondement des DSL Kotlin et des fonctions de portée (apply, run, with). apply retourne le receveur ; run retourne le résultat du lambda. Maîtrisez cela pour construire des DSL internes expressifs et type-safe.

kotlin
// Lambda with receiver: A.(B) -> C
val greet: String.(Int) -> String = { times -> repeat(times) { this + "!" } }
fun main() {
    println("Hello".greet(3))  // Hello!!!Hello!!!Hello!!!
    // 'this' is the String receiver
}
// Build DSLs with receiver
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun add(s: String) { parts.add(s) }
}
fun build(init: StringBuilder.() -> Unit) =
    StringBuilder().apply(init)
val sb = build { add("a"); add("b") }
16

Tests (JUnit 5, MockK, Turbine)

Bases de JUnit 5

JUnit 5 (Jupiter) est le framework de test standard pour Kotlin. @Test marque les tests ; @BeforeEach/@AfterEach exécutent le setup/teardown par test. Utilisez des noms avec backticks pour la lisibilité. @ParameterizedTest + @ValueSource exécutent un test avec plusieurs entrées. assertThrows vérifie les exceptions. @Disabled ignore des tests.

kotlin
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    lateinit var calc: Calculator
    @BeforeEach fun setup() { calc = Calculator() }
    @Test fun `add two numbers`() {
        assertEquals(5, calc.add(2, 3))
    }
    @Test fun `divide by zero throws`() {
        assertThrows<ArithmeticException> { calc.divide(10, 0) }
    }
    @ParameterizedTest
    @ValueSource(ints = [1, 2, 3])
    fun `positive numbers`(n: Int) { assertTrue(n > 0) }
    @Disabled("TODO") @Test fun skip() {}
}

MockK (Simulation)

MockK est la bibliothèque de simulation idiomatique de Kotlin (gère les classes final, coroutines, fonctions d'extension). every { } returns/throws stubbe ; verify { } vérifie les appels. Utilisez relaxed = true pour ignorer le stubbing des méthodes void. MockK supporte nativement les fonctions suspend via coEvery/coVerify—essentiel pour les tests de coroutines.

kotlin
import io.mockk.*
interface UserService { fun find(id: Int): String?; fun save(name: String): Int }
class Test {
    val mock = mockk<UserService>()
    @Test fun test() {
        every { mock.find(1) } returns "Alice"
        every { mock.find(any()) } returns null
        every { mock.save(any()) } throws RuntimeException("fail")
        verify { mock.find(1) }              // called once
        verify(exactly = 2) { mock.find(any()) }
        confirmVerified(mock)
    }
}
// relaxUnitFun / relaxed = true for void methods

Turbine (Tests de Flow)

Turbine est la bibliothèque standard pour tester Flow. test { } collecte dans un scope contrôlé ; awaitItem() vérifie la prochaine émission, awaitComplete()/awaitError() vérifient la terminaison. Il gère le timing et le temps virtuel correctement. Combinez avec runTest pour des tests Flow rapides, déterministes et adaptés aux coroutines.

kotlin
import app.cash.turbine.test
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlin.test.*
class FlowTest {
    @Test fun testFlow() = runTest {
        flowOf(1, 2, 3).test {
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            awaitComplete()
        }
    }
    @Test fun testError() = runTest {
        flow<Int> { throw RuntimeException("x") }.test {
            assertTrue(awaitError() is RuntimeException)
        }
    }
}

Tests de coroutines (runTest)

runTest utilise le temps virtuel—les délais sont instantanément sautés, rendant les tests async rapides et déterministes. Utilisez StandardTestDispatcher + advanceUntilIdle() pour un contrôle manuel. Injectez un TestDispatcher dans le code de production (au lieu de Dispatchers.Main) pour la testabilité. Évitez runBlocking dans les tests—runTest est la méthode moderne.

kotlin
import kotlinx.coroutines.test.*
import kotlinx.coroutines.*
import kotlin.test.*
class RepoTest {
    @Test fun fetch() = runTest {
        // Virtual time: delay(1000) completes instantly
        val result = async { delay(1000); "data" }.await()
        assertEquals("data", result)
    }
    @Test fun withDispatcher() = runTest {
        val scheduler = StandardTestDispatcher()
        val scope = CoroutineScope(scheduler)
        // advanceUntilIdle() runs pending coroutines
        scope.launch { /*...*/ }
        scheduler.advanceUntilIdle()
    }
}

Assertions et styles Kotest

Kotest offre des assertions expressives (shouldBe, shouldContain) et plusieurs styles de spec (StringSpec, FunSpec, BehaviorSpec pour BDD). Il s'intègre aux tests basés sur les propriétés et possède des matchers riches. Choisissez-le plutôt que JUnit pour des tests lisibles et idiomatiques Kotlin. Il peut fonctionner aux côtés de JUnit 5.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith
class MyTest : StringSpec({
    "addition works" {
        1 + 1 shouldBe 2
    }
    "list contains" {
        listOf(1, 2, 3) shouldContain 2
    }
    "string prefix" {
        "hello" shouldStartWith "he"
    }
})
// Other styles: FunSpec, BehaviorSpec, ShouldSpec
17

Programmation fonctionnelle avancée

Fonctions de portée (let, run, with, apply, also)

Les fonctions de portée diffèrent par le receveur (this vs it) et la valeur de retour (this vs résultat). apply/also retournent le receveur (chaînage, builders) ; let/run/with retournent le résultat du lambda (transformations). Utilisez apply pour la configuration, let pour les vérifications de null/transformations, also pour les effets de bord. N'en abusez pas—la lisibilité d'abord.

kotlin
data class User(var name: String, var age: Int)
fun main() {
    val u = User("A", 1)
    // apply: configure & return this (builders)
    val configured = u.apply { age = 30 }
    // let: transform, it = receiver, returns lambda result
    val name = u.let { it.name.uppercase() }
    // run: transform, this = receiver, returns result
    val s = u.run { "$name:$age" }
    // with: like run but called as function
    val str = with(u) { "$name:$age" }
    // also: side effects, returns this (logging/chaining)
    u.also { println("Created $it") }
}

Séquences (Évaluation paresseuse)

Les séquences s'évalent paresseusement—chaque élément traverse tout le pipeline avant que le suivant commence (comme Java Streams). Cela évite les collections intermédiaires et court-circuite (take/find). Utilisez-les pour les grands jeux de données ou les pipelines multi-étapes. Pour les petites listes, une List eager est souvent plus rapide (moins de surcoût). generateSequence construit des flux infinis.

kotlin
fun main() {
    // Eager: List processes each step fully
    val eager = (1..10).toList()
        .map { it * 2 }
        .filter { it > 5 }
    // Lazy: Sequence processes element-by-element
    val lazy = (1..10).asSequence()
        .map { println("map $it"); it * 2 }
        .filter { println("filter $it"); it > 5 }
        .take(2)
        .toList()  // only processes until 2 found
    // Use Sequence for large/pipelined data
    // generateSequence for infinite streams
    val naturals = generateSequence(1) { it + 1 }
}

Fonctions inline & reified

inline élimine la surcoût d'allocation d'objets lambda en inlinant le bytecode—critique pour les boucles chaudes. Cela permet aussi les retours non locaux (return depuis la fonction externe dans un lambda). Les paramètres de type reified rendent l'info de type générique disponible à l'exécution (T::class, is T), mais nécessitent inline. Utilisez-les pour des assistants type-safe comme filterIsInstance.

kotlin
// inline: copies bytecode at call site (no lambda overhead)
inline fun measure(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}
// non-local return: inline lambdas can return from outer fun
inline fun forEach(list: List<Int>, f: (Int) -> Unit) {
    for (i in list) f(i)
}
// reified: access generic type at runtime
inline fun <reified T> List<*>.filterIsInstance() =
    filter { it is T } as List<T>
val strs = listOf(1, "a", 2, "b").filterIsInstance<String>()

Fonctions d'ordre supérieur

Les fonctions d'ordre supérieur prennent ou retournent des fonctions. Elles sont l'épine dorsale du Kotlin fonctionnel—map, filter, fold, compose. Les types de fonction comme (Int) -> Int sont de première classe. compose construit des pipelines. Utilisez-les pour abstraire des patterns, permettre la réutilisation de code et écrire du code déclaratif. Marquez inline pour les chemins critiques en performance.

kotlin
fun <T, R> List<T>.mapTo(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) result.add(transform(item))
    return result
}
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int =
    { x -> f(g(x)) }
fun main() {
    val double = { x: Int -> x * 2 }
    val inc = { x: Int -> x + 1 }
    val doubleThenInc = compose(inc, double)
    println(doubleThenInc(3))  // 7
    listOf(1, 2, 3).mapTo { it * it }
}

Récursion & tailrec

tailrec convertit les fonctions tail-récursives (où la récursion est la dernière opération) en boucles, empêchant le débordement de pile. L'appel récursif doit être en position tail—pas de multiplication/addition en attente. Utilisez un paramètre accumulateur pour rendre les fonctions tail-récursives. Essentiel pour les boucles de style fonctionnel sur de grandes entrées.

kotlin
// Regular recursion: stack overflow on large n
fun factorial(n: Int): Long =
    if (n <= 1) 1 else n * factorial(n - 1)
// tailrec: compiler optimizes to a loop (no stack growth)
tailrec fun factorialTail(n: Int, acc: Long = 1): Long =
    if (n <= 1) acc else factorialTail(n - 1, acc * n)
// Fibonacci with tailrec
tailrec fun fib(n: Int, a: Long = 0, b: Long = 1): Long =
    when (n) { 0 -> a; 1 -> b; else -> fib(n - 1, b, a + b) }
fun main() {
    println(factorialTail(10000))  // no stack overflow
}
18

Génériques & Variance

Classes & fonctions génériques

Les génériques permettent un code type-safe et réutilisable. Les classes utilisent <T> ; les fonctions déclarent <T> avant le type de retour. Les contraintes (T : Entity) restreignent la borne du type. Contrairement à Java, les génériques de Kotlin sont reified pour les fonctions inline et ont une variance au site de déclaration, rendant les APIs génériques plus sûres et ergonomiques.

kotlin
class Box<T>(val value: T) {
    fun get(): T = value
}
fun <T> singletonList(item: T): List<T> = listOf(item)
fun main() {
    val intBox = Box(42)        // Box<Int> inferred
    val strBox = Box<String>("hi")
    val nums = singletonList(1)
}
// Generic constraints
class Repository<T : Entity> {
    fun save(item: T) { /* T is Entity subtype */ }
}
interface Entity { val id: Int }

Variance (in/out)

La variance contrôle les relations de sous-type des génériques. out (covariant) : un Producer<Dog> est un Producer<Animal>—sûr car vous ne lisez que T. in (contravariant) : un Sink<Animal> est un Sink<Dog>—sûr car vous n'écrivez que T. Les collections mutables sont invariantes (lecture + écriture). Utilisez out/in pour concevoir des APIs sûres et flexibles.

kotlin
// out (covariant): Producer<Sub> is Producer<Super>
interface Source<out T> { fun next(): T }
// in (contravariant): Consumer<Super> is Consumer<Sub>
interface Sink<in T> { fun put(item: T) }
// invariant (default): neither
class MutableList<T> {
    fun add(item: T) {}
    fun get(): T = TODO()
}
open class Animal
class Dog : Animal()
val src: Source<Animal> = Source<Dog>()  // OK (out)
val sink: Sink<Dog> = Sink<Animal>()     // OK (in)

Variance en pratique

Les types de fonction utilisent la variance automatiquement : les paramètres sont 'in', les types de retour sont 'out'. C'est pourquoi (Dog) -> Unit est assignable à (Animal) -> Unit. Le principe PECS (Producer Extends, Consumer Super) correspond à out/in de Kotlin. Concevez des interfaces génériques avec out quand elles ne produisent que T, in quand elles ne consomment que T.

kotlin
// Function types are variant by nature
// (T) -> R is contravariant in T, covariant in R
val dogHandler: (Dog) -> Unit = { println(it) }
val animalHandler: (Animal) -> Unit = dogHandler  // OK
// Covariant return types
interface Repository<out T> { fun find(id: Int): T }
class DogRepo : Repository<Dog> {
    override fun find(id: Int): Dog = Dog()
}
val repo: Repository<Animal> = DogRepo()  // OK
// PECS rule: Producer extends, Consumer super
// Kotlin: out = producer, in = consumer

Projections de type & projection étoile

Les projections de type rendent temporairement un type variant au site d'usage. Array<out Any> signifie 'Array d'un sous-type d'Any, lisible'. Array<in Any> signifie 'inscriptible comme Any'. La projection étoile (*) signifie 'un type inconnu'—utile quand vous n'avez besoin que de size/contains, pas du type d'élément. Comme les wildcards de Java.

kotlin
fun copy(from: Array<out Any>, to: Array<in Any>) {
    // from: only read (covariant projection)
    // to: only write (contravariant projection)
    for (i in from.indices) to[i] = from[i]
}
// Star projection: unknown type, read-only
fun printSize(list: List<*>) {
    println(list.size)  // OK (no type needed)
    // list[0]  // type is Any? — limited use
}
val mixed: List<*> = listOf(1, "a", 3.0)
// Use when type is irrelevant or unknown

Paramètres de type reified

reified préserve l'info de type générique à l'exécution (normalement effacée sur JVM). Il nécessite inline (le type est connu à chaque site d'appel). Permet les vérifications is T, T::class et filterIsInstance. Sans reified, vous devriez passer un paramètre Class<T> manuellement. Limitation : les types reified ne peuvent être utilisés dans les fonctions non-inline ou comme paramètres de type de classe.

kotlin
// reified requires inline
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
    filter { it is T } as List<T>
inline fun <reified T> Any.castTo(): T = this as T
inline fun <reified T> typeOf() = T::class.simpleName
fun main() {
    val list = listOf(1, "a", 2, "b", 3)
    val strs: List<String> = list.filterIsInstance()
    println(strs)  // [a, b]
    println(typeOf<String>())  // String
    // Without reified, T is erased at runtime
    // reified makes T::class and 'is T' available
}
19

Sécurité null avancée

Types nullables

Kotlin distingue les types nullables (T?) et non-null (T) à la compilation. ?. l'appel sûr retourne null si le receveur est null. ?: Elvis fournit une valeur par défaut. !! lève une NPE (à éviter). Le compilateur impose les vérifications de null, éliminant NullPointerException dans le code Kotlin.

kotlin
var name: String = "Alice"  // Non-null
// name = null  // Error
var nickname: String? = null  // Nullable
// Safe call
println(nickname?.length)  // null if nickname is null
// Elvis operator
val len = nickname?.length ?: 0  // 0 if null
// Not-null assertion (use sparingly)
val len2 = nickname!!.length  // NPE if null

let & run

let exécute un bloc si la valeur est non-null (it = la valeur). run appelle un bloc avec l'objet comme receveur, retourne le résultat. apply configure un objet, retourne l'objet. also effectue des effets de bord, retourne l'objet. Ces fonctions de portée réduisent les vérifications de null et améliorent la lisibilité.

kotlin
val name: String? = "Alice"
name?.let {
    println("Length: ${it.length}")  // Only if not null
}
// run: object + block
val result = "Hello".run {
    length  // Returns 5
}
// apply: configure object
val list = mutableListOf<Int>().apply {
    add(1); add(2); add(3)
}

lateinit

lateinit diffère l'initialisation des propriétés non-null. Ne peut être utilisé avec les primitifs ou types nullables. Lève UninitializedPropertyAccessException si accédé avant init. ::prop.isInitialized vérifie. Utile pour l'injection de dépendances et les propriétés gérées par cycle de vie.

kotlin
class Service {
    lateinit var dependency: Database
    fun init() {
        dependency = connectDatabase()
    }
    fun use() {
        if (::dependency.isInitialized) {
            dependency.query()
        }
    }
}

Collections nullables

Les collections peuvent contenir des éléments nullables (List<String?>) ou être nullables (List<String>?). filterNotNull supprime les null. firstOrNull retourne null au lieu de lever une exception. Gérez les éléments nullables avec des appels sûrs. Soyez explicite sur la nullabilité dans les types de collection.

kotlin
val list: List<String?> = listOf("a", null, "b")
val filtered = list.filterNotNull()  // ["a", "b"]
val lengths = list.map { it?.length }  // [1, null, 1]
val first = list.firstOrNull()  // "a" or null
val firstNonEmpty = list.firstOrNull { it?.isNotEmpty() == true }

Types plateforme

Les types plateforme proviennent de l'interop avec Java où la nullabilité est inconnue. Kotlin ne peut pas imposer la sécurité null pour eux. Déclarez toujours explicitement les types nullables pour les valeurs de retour Java. Utilisez les annotations @Nullable/@NotNull en Java. Les annotations JSR-305 aident Kotlin à inférer la nullabilité.

kotlin
// Java interop: platform type (no null info)
val name: String = javaObject.getName()  // May be null!
// Kotlin does not know if Java returns null
// Fix: explicit nullable type
val name2: String? = javaObject.getName()
// Or @Nullable annotation in Java
20

Construction de DSL

Builder avec receveur lambda

Le lambda avec receveur (T.() -> Unit) permet la syntaxe DSL. Dans le lambda, this est l'objet receveur. Les méthodes peuvent être appelées sans qualification. C'est ainsi que Kotlin construit des DSL type-safe comme Gradle, HTML et les builders SQL.

kotlin
class StringBuilder {
    private val parts = mutableListOf<String>()
    fun line(s: String) { parts.add(s) }
    fun build() = parts.joinToString("\n")
}
fun buildString(init: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.init()  // Execute lambda with sb as receiver
    return sb.build()
}

DSL HTML

Le DSL HTML utilise des builders imbriqués avec receveurs lambda. Chaque balise est une fonction qui crée un builder enfant. Le lambda configure l'enfant. Produit du HTML type-safe et composable. kotlinx.html est une implémentation réelle. Le même pattern fonctionne pour toute structure hiérarchique.

kotlin
fun html(init: HTML.() -> Unit): HTML {
    val h = HTML(); h.init(); return h
}
class HTML {
    fun body(init: Body.() -> Unit) { /* ... */ }
}
html {
    body {
        // this: Body
        p("Hello")
    }
}

@DslMarker

@DslMarker empêche l'accès implicite au receveur des portées externes. Sans lui, les méthodes de HTML et Body sont toutes accessibles, causant de la confusion. L'annotation restreint l'accès au receveur le plus interne. Rend les DSL type-safe et non ambigus. Essentiel pour les DSL complexes.

kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML { fun body(...) {} }
@HtmlDsl
class Body { fun p(...) {} }
html {
    body {
        // p()  // OK: in Body scope
        // body()  // Error: in Body scope, not HTML
    }
}

DSL Gradle

Le DSL Kotlin de Gradle utilise le même pattern de builder. plugins, dependencies sont des fonctions avec lambdas receveur. implementation, testImplementation sont des fonctions de configuration de dépendances. Type-safe : le compilateur vérifie les noms de fonction et les types de paramètres. Bien meilleur que Groovy pour le refactoring.

kotlin
plugins {
    kotlin("jvm") version "1.9.0"
}
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}
// All are function calls with lambda receivers

Anko SQL

Anko (et Exposed) fournissent des DSL SQL type-safe. Les comparaisons de colonnes sont typées. Impossible de comparer une colonne string à un int. Le DSL génère le SQL. Empêche l'injection SQL et les erreurs de type. Le même pattern s'applique à tout langage spécifique au domaine.

kotlin
fun users(where: SqlExpressionBuilder.() -> Op<Boolean>) {
    // DSL for SQL queries
}
users {
    (Users.age greater 18) and (Users.name like "A%")
}
// Type-safe SQL: compiler checks column types
21

Collections

Opérations sur List

Les collections Kotlin ont des APIs fonctionnelles riches. map, filter, reduce sont standard. groupBy partitionne par clé. chunked divise en listes de taille fixe. windowed crée des fenêtres glissantes. Toutes retournent de nouvelles collections. Utilisez asSequence() pour l'évaluation paresseuse.

kotlin
val list = listOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, n -> acc + n }
val grouped = list.groupBy { it % 2 }  // {0=[2,4], 1=[1,3,5]}
val chunked = list.chunked(2)  // [[1,2],[3,4],[5]]

Sequence

Sequence est une collection paresseuse (comme Java Stream). Les opérations sont évaluées à la demande. Pas de collections intermédiaires. Efficace pour les grandes collections avec opérations chaînées. Utilisez asSequence() pour convertir. Forcez avec toList(), toSet(), etc.

kotlin
val result = (1..1000000).asSequence()
    .map { it * 2 }
    .filter { it > 100 }
    .take(10)
    .toList()
// Lazy: only computes what is needed
// No intermediate collections

Destructuration

La destructuration sépare les objets en variables. Fonctionne avec Pair, Triple, data classes et entrées de Map. Les fonctions componentN() le permettent. withIndex() associe index et valeur. Utile pour les valeurs de retour multiples et l'itération. Les data classes génèrent automatiquement componentN.

kotlin
val (a, b) = Pair(1, "hello")
val (x, y, z) = Triple(1, 2, 3)
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}
data class Point(val x: Int, val y: Int)
val (px, py) = Point(1, 2)

Mutable vs Immutable

Kotlin a des collections mutables (MutableList) et immuables (List). Préférez immuable pour la sécurité. toList() crée une copie immuable. toMutableList() crée une copie mutable. Les interfaces immuables n'exposent pas de méthodes de mutation, empêchant la modification accidentelle.

kotlin
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)  // OK
val immutable = listOf(1, 2, 3)
// immutable.add(4)  // Error: no add method
// Convert
val imm = mutable.toList()
val mut = imm.toMutableList()

Associate & Partition

toMap/associate convertissent en maps. partition divise en deux listes par prédicat. flatten fusionne les listes imbriquées. flatMap mappe et aplatit. Ces opérations remplacent les boucles verbeuses par des expressions déclaratives. Toutes retournent de nouvelles collections.

kotlin
val list = listOf("a" to 1, "b" to 2)
val map = list.toMap()  // {a=1, b=2}
val byLength = list.associate { it.first to it.second }
val (evens, odds) = list.partition { it.second % 2 == 0 }
// evens: [(a,1)?] odds: [(b,2)]
val flat = listOf(listOf(1), listOf(2, 3)).flatten()  // [1,2,3]
22

Pièges courants

== vs ===

Kotlin == appelle equals (égalité de valeur), contrairement à Java. === vérifie l'égalité de référence. Utilisez == pour la comparaison de valeurs. === est rarement nécessaire. Pour Integer, les valeurs -128 à 127 sont mises en cache, donc === peut être vrai ou faux. Utilisez toujours == pour les valeurs.

kotlin
val a = Integer(127)
val b = Integer(127)
a == b  // true (value equality, calls equals)
a === b  // true (Integer caches -128 to 127)
val c = Integer(128)
val d = Integer(128)
c == d  // true
c === d  // false (not cached)

Companion Object

Kotlin n'a pas de membres statiques. companion object contient les méthodes et constantes "statiques". const val est une vraie constante de compilation. Le companion object est une instance singleton. @JvmStatic rend les méthodes appelables comme statiques depuis Java. Utilisez des fonctions de premier niveau pour de vrais statiques.

kotlin
class MyClass {
    companion object {
        const val CONSTANT = 42
        fun create() = MyClass()
    }
}
MyClass.CONSTANT  // 42
MyClass.create()  // Static-like access
// companion object is a real object, not static

Copie de data class

Les data classes génèrent automatiquement copy() qui crée une copie modifiée. L'original est inchangé (immuable). Seuls les champs spécifiés changent. Utile pour les mises à jour. Combiné à la destructuration, les data classes sont puissantes pour modéliser les données. Évitez var dans les data classes pour l'immuabilité.

kotlin
data class User(val name: String, val age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // New instance
// alice is unchanged
// copy uses named arguments for changed fields

Exhaustivité des classes sealed

Les classes sealed restreignent les sous-types au même fichier/package. when sur les classes sealed est exhaustif : le compilateur avertit si une branche manque. Pas de else nécessaire si tous les cas sont couverts. Ajouter un nouveau sous-type provoque des avertissements partout. Idéal pour les machines à états et les résultats.

kotlin
sealed class Result
data class Success(val value: Int) : Result()
data class Failure(val error: String) : Result()
fun handle(r: Result) = when (r) {
    is Success -> r.value
    // Warning: Missing Failure branch
}
// Fix: handle all cases
fun handle(r: Result) = when (r) {
    is Success -> r.value
    is Failure -> 0
}  // No else needed

Extension vs Membre

Les fonctions membre ont toujours priorité sur les fonctions d'extension avec la même signature. Les extensions sont résolues à la compilation (dispatch statique), les membres à l'exécution (dispatch dynamique). Les extensions ne peuvent pas surcharger les membres. Utilisez les extensions pour les fonctions utilitaires, pas pour le polymorphisme.

kotlin
class Foo {
    fun bar() = "member"  // Member function
}
fun Foo.bar() = "extension"  // Extension function
Foo().bar()  // "member" - members win!
// Extensions are resolved statically
// Members are resolved dynamically (virtual)
23

Kotlin Multiplatform

Code commun

Kotlin Multiplatform (KMP) partage du code entre plateformes. expect/actual déclare des implémentations spécifiques à chaque plateforme. commonMain contient le code partagé. Les source sets spécifiques à chaque plateforme implémentent actual. Partagez la logique métier, gardez l'UI native. Gradle configure les cibles.

kotlin
// commonMain/src/Platform.kt
expect fun getPlatformName(): String
// androidMain/src/Platform.kt
actual fun getPlatformName(): String = "Android"
// iosMain/src/Platform.kt
actual fun getPlatformName(): String = "iOS"
// Shared business logic
class Greeting {
    fun greet() = "Hello from ${getPlatformName()}"
}

Module partagé

Les projets multiplateformes utilisent le bloc kotlin dans Gradle. Définissez les cibles (android, ios). commonMain a les dépendances partagées. Les source sets de plateforme peuvent avoir des deps spécifiques. iOS utilise Kotlin/Native pour la compilation directe. Partagez la logique, pas l'UI.

kotlin
// build.gradle.kts (shared module)
kotlin {
    androidTarget()
    iosX64(); iosArm64(); iosSimulatorArm64()
    sourceSets {
        val commonMain by getting {
            dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") }
        }
    }
}

Réseau (Ktor)

Ktor est un client HTTP multiplateforme. L'API est commune, le moteur est spécifique à la plateforme. Android utilise le moteur OkHttp/Android. iOS utilise Darwin. Les coroutines fonctionnent entre plateformes. Partagez le réseau, la base de données (SQLDelight) et la logique métier.

kotlin
// Shared HTTP client
suspend fun fetchUser(): User {
    return client.get("https://api.example.com/user").body()
}
// Platform-specific engine
// Android: OkHttp or Android
// iOS: Darwin
// Common code uses the same API

SQLDelight

SQLDelight génère du Kotlin type-safe depuis SQL. Les fichiers .sq contiennent le SQL avec des requêtes nommées. Génère des objets de requête type-safe. Fonctionne entre plateformes (Android, iOS, JVM). Le SQL est la source de vérité. Les migrations de schéma sont suivies. Alternative à Room pour le multiplateforme.

kotlin
// .sq file: User.sq
CREATE TABLE User (id INTEGER, name TEXT);
selectById: SELECT * FROM User WHERE id = ?;
insertUser: INSERT INTO User (id, name) VALUES (?, ?);
// Generated Kotlin code
val queries: UserQueries = database.userQueries
queries.insertUser(1, "Alice")
val user = queries.selectById(1).executeAsOne()

Compose Multiplatform

Compose Multiplatform étend Jetpack Compose à iOS, Desktop et Web. Partagez le code UI entre plateformes. Même API @Composable. Points d'entrée spécifiques à la plateforme. Encore expérimental pour iOS. Réduit la duplication d'UI. Plugin Gradle : compose-multiplatform.

kotlin
// Shared UI with Compose Multiplatform
@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}
// Android
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { Greeting("World") }
    }
}
// Desktop
fun main() = application {
    Window { Greeting("Desktop") }
}
24

Tests Kotlin

JUnit 5

JUnit 5 est le framework de test standard. Kotlin permet des noms de test avec backticks pour la lisibilité. assertEquals, assertThrows sont des assertions courantes. @BeforeEach, @AfterEach pour le setup/teardown. @ParameterizedTest pour les tests data-driven. Utilisez kotlin.test pour le multiplateforme.

kotlin
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
    @Test
    fun `test addition`() {
        val calc = Calculator()
        assertEquals(5, calc.add(2, 3))
    }
    @Test
    fun `throws on negative`() {
        assertThrows<IllegalArgumentException> { calc.sqrt(-1) }
    }
}

MockK

MockK est une bibliothèque de simulation native Kotlin. Supporte les classes final et les fonctions d'extension. every stubbe, verify vérifie les appels. @MockK crée des mocks, @InjectMockKs les injecte. coEvery/coVerify pour les coroutines. Meilleur support Kotlin que Mockito.

kotlin
class UserServiceTest {
    @MockK
    lateinit var repo: UserRepository
    @InjectMockKs
    lateinit var service: UserService
    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        every { repo.find(1) } returns User("Alice")
    }
    @Test
    fun `test find`() {
        assertEquals("Alice", service.find(1).name)
        verify { repo.find(1) }
    }
}

Kotest

Kotest est un framework de test Kotlin-first. Plusieurs styles : StringSpec, BehaviorSpec, FunSpec. shouldBe est une assertion fluente. Tests de propriétés avec Arb. Supporte les tests data-driven. S'intègre avec Spring et Ktor. Plus idiomatique Kotlin que JUnit.

kotlin
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class CalculatorTest : StringSpec({
    "addition should work" {
        Calculator().add(2, 3) shouldBe 5
    }
    "should throw on negative" {
        shouldThrow<IllegalArgumentException> { Calculator().sqrt(-1) }
    }
})

Tests de coroutines

runTest de kotlinx-coroutines-test fournit le temps virtuel. Les délais sont sautés instantanément. advanceUntilIdle exécute les coroutines en attente. Bien plus rapide que le temps réel. Utilisez TestDispatcher pour un contrôle fin. La bibliothèque Turbine teste les émissions de Flow.

kotlin
@Test
fun `test async`() = runTest {
    val result = fetchData()
    assertEquals("data", result)
}
// runTest replaces runBlocking with virtual time
// advanceUntilIdle() runs all pending coroutines
// Delay skips instantly

Turbine (Tests de Flow)

Turbine teste les émissions de Flow. awaitItem obtient la prochaine émission. awaitComplete vérifie la complétion. awaitError vérifie une erreur. Le bloc test se suspend jusqu'à ce que le flux se termine. Bien plus propre que la collecte manuelle. Essentiel pour les tests de Flow.

kotlin
@Test
fun `test flow`() = runTest {
    flowOf(1, 2, 3).test {
        awaitItem() shouldBe 1
        awaitItem() shouldBe 2
        awaitItem() shouldBe 3
        awaitComplete()
    }
}
// Test errors
flow<Int> { throw Exception() }.test {
    awaitError() shouldBe Exception()
}
25

Coroutines avancées

Portée de coroutine

CoroutineScope définit une durée de vie pour les coroutines. viewModelScope s'annule automatiquement à la destruction du VM. SupervisorJob empêche les échecs enfants d'annuler les frères. Les scopes personnalisés nécessitent une annulation explicite. Les scopes propagent l'annulation aux enfants. N'utilisez jamais GlobalScope en production (non annulable).

kotlin
class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = api.getData()
            _data.value = data
        }
    }
}
// Custom scope
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { /* ... */ }
scope.cancel()  // Cancel all children

Dispatchers

Les Dispatchers routent les coroutines vers les pools de threads. Main : thread UI (Android). IO : E/S bloquantes (64+ threads). Default : travail CPU (nombre de CPU threads). Unconfined : thread appelant (avancé). withContext change de dispatcher temporairement. Choisir le bon dispatcher améliore les performances et empêche les gelures UI.

kotlin
launch(Dispatchers.Main) { updateUI() }
launch(Dispatchers.IO) { writeFile() }
launch(Dispatchers.Default) { heavyComputation() }
launch(Dispatchers.Unconfined) { runAnywhere() }
// Switch context
withContext(Dispatchers.IO) {
    val data = readDisk()
}

Opérateurs de Flow

Opérateurs de Flow : map transforme, filter sélectionne, flatMapMerge/flatMapConcat chaîne les flux. Les cold flows démarrent frais à chaque collecteur. StateFlow détient une valeur (comme LiveData). SharedFlow diffuse à plusieurs collecteurs. stateIn convertit cold en hot. buffer/conflate contrôlent la backpressure.

kotlin
flowOf(1, 2, 3, 4, 5)
    .map { it * 2 }
    .filter { it > 4 }
    .collect { println(it) }  // 6, 8, 10

// Cold flow: emits per collector
// StateFlow: hot, stateful
// SharedFlow: hot, broadcast
val state = MutableStateFlow(0)
state.value = 1

Gestion des exceptions

CoroutineExceptionHandler attrape les exceptions non capturées dans launch. Les exceptions async se propagent vers await. SupervisorJob isole les échecs enfants. CancellationException est spécial : relancée, non attrapée par catch. N'avalez jamais CancellationException. Utilisez try/finally ou use() pour le nettoyage. L'annulation se propage à travers les appels suspend.

kotlin
val handler = CoroutineExceptionHandler { _, e ->
    Log.e("TAG", "Caught: $e", e)
}
scope.launch(handler) {
    throw RuntimeException("oops")
}
// try/catch in coroutine
try {
    riskyCall()
} catch (e: Exception) {
    // handle
}

Channels

Les Channels permettent la communication coroutine-à-coroutine. send se suspend quand plein, receive se suspend quand vide. capacity : buffered (RENDEZVOUS=0, UNLIMITED, CONFLATED). close() signale la complétion. produce crée une coroutine producteur. Channel est hot : les valeurs sont consommées une fois. Préférez Flow pour la plupart des usages.

kotlin
val channel = Channel<Int>(capacity = 10)
launch {
    for (i in 1..5) channel.send(i)
    channel.close()
}
launch {
    for (x in channel) println(x)
}
// Produce pattern
fun numbers() = produce {
    for (i in 1..10) send(i)
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.