Aller au contenu
KotlinBack

Kotlin 2.2 : toutes les nouveautés du langage

Kotlin 2.2.0 est sorti il y a quelques mois maintenant, mais peu d'articles résument les nouveautés qui ont enrichi le langage. Petit tour d'horizon de ces nouveautés.

What's new in Kotlin 2.2.0

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.

KotlinConf′25 : quelles sont les annonces à retenir ?
À la KotlinConf′25, JetBrains a levé le voile sur l’avenir de Kotlin : langage, IA, multiplateforme, outillage… Voici ce qu’il faut retenir.

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 :

Dernier