Guard conditions : des conditions multiples dans vos expressions when
Les guard conditions permettent de définir plusieurs conditions dans les déclarations ou expressions when. Bon, ok, cela ne semble pas très intéressant dit comme ça. Reprenons depuis le début.
En Kotlin, le mot-clé when est l'équivalent du switch-case en Java. Il peut être utilisé en déclaration pour exécuter un bloc de code de manière conditionnelle ou en expression pour attribuer une valeur à une variable (comme cela est possible aussi en Java depuis la version 13). Voici des exemples simples pour mettre en contexte :
fun temperatureFeeling(temperature: Int) = when (temperature) {
// Remarquer qu'en Kotlin, il n'y a pas de 'break' keyword
// car uniquement une branche est exécutée
0 -> println("The water is becoming ice")
// Les conditions des branches peuvent être variées
in -50..-1 -> println("freezing cold")
in 1..15 -> println("cold")
in 35..60 -> println("super hot")
}
fun main() {
// La fonction ne renvoie rien
listOf(-50, 0, 5, 18, 45).forEach { temperatureFeeling(it) }
// output
// freezing cold
// The water is becoming ice
// cold
// kinda cozy
// super hot
}when as a declaration
enum class TemperatureFeeling {
FREEZING_COLD,
THE_WATER_IS_BECOMING_ICE,
COLD,
KINDA_COZY,
SUPER_HOT
}
fun setTemperature(temperature: TemperatureFeeling): Int = when (temperature) {
TemperatureFeeling.FREEZING_COLD -> -50
TemperatureFeeling.THE_WATER_IS_BECOMING_ICE -> 0
TemperatureFeeling.COLD -> 5
TemperatureFeeling.KINDA_COZY -> 22
TemperatureFeeling.SUPER_HOT -> 50
}
fun main() {
// Ici, la fonction renvoie un entier et affiche cette valeur
TemperatureFeeling.entries.forEach { println(setTemperature(it)) }
// output
// -50
// 0
// 5
// 22
// 50
}when as an expression
L'une des utilisations très pratiques du when en Kotlin est d'agir en fonction du type d'un objet :
// Une interface (ou classe) marquée comme 'sealed' ne peut être étendue
// uniquement par des classes connues lors de la compilation et définies dans le même package.
// Cela permet de contrôler les classes qui en héritent
// mais aussi de pouvoir être exhaustif dans l'utilisation la définition des branches d'un `when`.
sealed class Animal(val name: String)
data class Cat(val catName: String) : Animal(catName)
data class Fish(val fishName: String, val liveInOcean: Boolean) : Animal(fishName)
fun displayAnimalName(animal: Animal) = when(animal) {
is Cat -> println("The cat's name is ${animal.name}")
// Remarquer que le compilateur infère le type d'animal (liveInOcean est disponible)
is Fish -> println("The fish's name is ${animal.name} and it lives in the ocean [${animal.liveInOcean}]")
}
fun main() {
// listOf crée une liste immuable d'éléments
// de type Animal, le type est inféré
listOf(Cat("Fluffy"), Fish("Titanic", true), Fish("Nemo", false))
// Pour chaque Animal défini, nous appelons la fonction
.forEach { displayAnimalName(it) }
// output
// The cat's name is Fluffy
// The fish's name is Titanic and it lives in the ocean [true]
// The fish's name is Nemo and it lives in the ocean [false]
}when for checking value type
Et nous y voilà ! Nous avons toutes les bases pour comprendre exactement ce que sont les guard conditions. Elles permettent d'inclure plus qu'une condition pour une branche du when.
sealed interface Vehicule
data class Car(val numberOfPassagers: Int) : Vehicule
data class Truck(val hasATrailer: Boolean) : Vehicule
fun displayCar(number: Int) = println("This is a car with $number passagers")
fun displayTruck() = println("This is a truck")
fun trailerOrPassagers(vehicule: Vehicule) = when(vehicule) {
is Car -> displayCar(vehicule.numberOfPassagers)
// Le guard est ici, après le 'if'
is Truck if vehicule.hasATrailer -> displayTruck()
else -> println("This is something else!")
}
fun main() {
// listOf crée une liste immuable d'éléments
// de type Vehicule, le type est inféré
listOf(Car(4), Truck(true), Truck(false))
// Pour chaque véhicule définit, nous appelons la fonction
.forEach { trailerOrPassagers(it) }
// output
// This is a car with 4 passagers
// This is a truck
// This is something else!
}guard conditions
Cela semble une utilisation fort spécifique. Et je ne pourrai pas vous contredire. Mais probablement que l'on verra d'autres usages avec les RichError annoncées lors de la KotlinConf'2025, qui devraient arriver avec Kotlin 2.4. Cela permet donc de faire des choses beaucoup plus complexes mais de manière concise dans les conditions des différentes branches.

Multi-dollar string interpolation : simplifiez vos interpolations multi-lignes
Cette fonctionnalité est beaucoup plus facile à appréhender. L'interpolation d'une variable dans un string est très concise en Kotlin. Il suffit de précéder la variable d'un symbole $ et éventuellement de l'entourer d'accolades s'il s'agit d'une expression. Pour afficher un dollar, on peut évidemment l'échapper avec un backslash.
val name = "John"
println("Hi $name. Your name has ${name.length} characters")
println("I want to display a dollar \$")escaped string
Il existe les strings multi-lignes. Ils ne contiennent aucun échappement et peuvent contenir des nouvelles lignes et tout autre caractère. Puisqu'il n'y a aucun échappement, afficher un symbole $ n'était pas très pratique (→ ${'$'})
println("""
I am a really long text
and I need to be displayed
on several lines
""")
println("""But it is annoying to display ${'$'}""")multiline string
La nouvelle interpolation permet de résoudre ce souci. Elle permet de définir combien de symboles dollar consécutifs doivent apparaître pour déclencher une interpolation.
val name = "John"
println($$"""
But it is not a problem anymore $ !!!
What do you think, $$name ?
""")multi-dollar interpolation
Cela est très pratique dans tous les cas où il faut utiliser littéralement un dollar.
Non-local break and continue : plus de flexibilité dans vos boucles
Jusqu'à maintenant, le code suivant ne compilait pas :
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8)
for (number in numbers) {
number.takeIf { it % 2 == 0 } // Retourne l'élément s'il satisfait la condition (sinon null)
?.let { // Lambda qui est exécutée si l'élément n'est pas null
continue // compilation error
}
println(number)
}before Kotlin 2.2.0
Ce bout de code devrait permettre de n'afficher que les nombres impairs, d'une façon très idiomatique. Mais on remarque que le compilateur n'accepte pas le continue à l'intérieur d'une lambda pour interagir avec la boucle qui l'entoure. Pour que ce bout de code fonctionne, il fallait oublier la façon idiomatique :
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8)
for (number in numbers) {
if (number % 2 == 0) {
continue
}
println(number)
}non idiomatic way
Bon, pour un exemple aussi simple, ce n'est pas dérangeant... mais pour des fonctions bien plus complexes, cela pourrait poser problème. Autre exemple
@JvmInline
value class Element(val name: String) {
fun filterLength(): String? = if (name.length == 3) null else name.reversed()
}
fun main() {
val elements = listOf(
Element("foo"),
Element("bar"),
Element("John"),
Element("Something else")
)
println(processElements(elements))
}
fun processElements(elements: List<Element>): Boolean {
for (element in elements) {
// filterLength peut renvoyer null
val reversed = element.filterLength() ?: run {
// la scoped function run permet d'exécuter un block de code
// et permet généralement d'exécuter des side effects (log par exemple)
println("Skipping '${element.name}'")
continue // erreur de compilation ici
}
if (reversed == "nhoJ") return true
}
return false
}continue in scoped function
Kotlin 2.2.0 corrige cela et il n'y a plus d'incohérence. Nous avons le comportement attendu : les continue et break définis dans les lambdas permettent d'interagir avec les boucles dans lesquelles ces lambdas sont appelées.
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8)
for (number in numbers) {
number.takeIf { it % 2 == 0 }?.let { continue }
println(number)
}non-local continue works as expected
Tout cela est aussi valable pour sortir d'une boucle avec break. En revanche, les non-local break and continue sont applicables uniquement pour les inline functions, c'est-à-dire les fonctions dont le corps est directement remplacé aux endroits où elles sont appelées lors de la compilation.
Base64 : l'API d'encodage enfin en stable
Introduite en preview avec Kotlin 1.8.20, l'API kotlin.io.encoding.Base64 est enfin stable ! Voici un exemple très basique d'utilisation.
import kotlin.io.encoding.Base64
fun main() {
val encoded = Base64.encode("This string is not encoded".toByteArray())
println(encoded) // SGVsbG8sIFdvcmxkIQ==
val decoded = Base64.decode(encoded)
println(decoded.decodeToString()) // This string is not encoded
}Base64 API
Comment migrer vers Kotlin 2.2
Rien de plus simple : il suffit de changer la version dans les fichiers des outils de build correspondants :
plugins {
// Replace `<...>` with the plugin name appropriate for your target environment
kotlin("<...>") version "2.2.20"
// For example, if your target environment is JVM:
// kotlin("jvm") version "2.2.20"
// If your target is Kotlin Multiplatform:
// kotlin("multiplatform") version "2.2.20"
}Gradle: build.gradle.kts
<properties>
<kotlin.version>2.2.20</kotlin.version>
</properties>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
</plugin>
</plugins>Maven: pom.xml
Fonctionnalités en preview
D'autres fonctionnalités sont disponibles en preview comme les typealias qui pourront être imbriqués dans des classes ou encore la context-sensitive resolution... mais gardons ça pour quand ça sera stable.
Pour aller plus loin
Si vous avez envie de voir Sebastian, Alejandro et Michail présenter toutes les fonctionnalités de manière plus détaillée, c'est par ici que ça se passe :
