Kotlin 1.4.30 s'accompagne d'un nouveau backend JVM
et de nouvelles fonctionnalités de langage et multiplateformes

Kotlin 1.4.30 est maintenant disponible. Il s’agit de la dernière version incrémentielle en 1.4, il y a donc beaucoup de nouvelles fonctionnalités expérimentales que JetBrains prévoit de stabiliser dans la version 1.5.0. Passons en revue quelques nouveautés et améliorations apportées par cette version.

Caractéristiques du langage et compilateur

Compilateur

Le nouveau backend de la JVM passe en phase bêta et produit maintenant des binaires stables. Vous pouvez donc l’utiliser en toute sécurité dans vos projets.

JetBrains a travaillé sur l’implémentation d’un nouveau backend IR pour la JVM dans le cadre de notre projet de réécriture de l’ensemble du compilateur. En fournissant une infrastructure polyvalente permettant d’ajouter facilement de nouvelles caractéristiques au langage, ce nouveau compilateur améliorera les performances tant pour les utilisateurs de Kotlin que pour l’équipe Kotlin elle-même.

Le travail de JetBrains sur le backend IR de la JVM est presque terminé et l'éditeur va bientôt le faire passer en version stable.

Ce qui change avec le nouveau backend :
  • JetBrains a corrigé un certain nombre de bogues présents dans l’ancien backend.
  • Le développement de nouvelles fonctionnalités du langage sera beaucoup plus rapide.
  • JetBrains ajoutera toutes les futures améliorations de performance au nouveau backend de la JVM.
  • Le nouveau Jetpack Compose ne fonctionnera qu’avec le nouveau backend.

Autre argument en faveur de l’utilisation du nouveau backend IR de la JVM : il deviendra la valeur par défaut dans Kotlin 1.5.0. Avant cela, JetBrains veut s'assurer de corriger autant de bogues que possible ; en adoptant le nouveau backend rapidement, vous contribuerez à optimiser la fluidité de cette migration.

Aperçu des nouvelles caractéristiques du langage

Parmi les nouvelles caractéristiques du langage que JetBrains va publier dans Kotlin 1.5.0 figurent les classes de valeurs inline, les enregistrements de la JVM et les interfaces scellées.

Stabilisation des classes de valeurs inline

Les classes inline sont disponibles en alpha depuis Kotlin 1.3 et passent en bêtadans la version 1.4.30.

Kotlin 1.5 stabilise le concept de classes inline mais l’intègre à une fonctionnalité plus générale, les classes de valeurs, que nous décrirons plus loin dans ce post.

Commençons par rappeler le fonctionnement des classes inline. Si vous connaissez déjà les classes inline, vous pouvez passer cette section et consulter directement les dernières modifications.

Pour rappel, une classe inline élimine une enveloppe (wrapper) autour d’une valeur :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
inline class Color(val rgb: Int)

Une classe inline peut être une enveloppe à la fois pour un type primitif et pour tout type de référence, comme String.

Le compilateur remplace les instances de classe inline (dans notre exemple, l’instance Color) par le type sous-jacent (Int) dans le bytecode, lorsque c’est possible :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
fun changeBackground(color: Color) 
val blue = Color(255)
changeBackground(blue)

En coulisse, le compilateur génère la fonction changeBackground avec un nom modifié prenant un paramètre Int et envoie directement la constante 255 sans créer d’enveloppe au point de l’appel :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
fun changeBackground-euwHqFQ(color: Int) 
changeBackground-euwHqFQ(255) // no extra object is allocated!

Le nom est altéré afin de permettre que la surcharge des fonctions prenant des instances de plusieurs classes inline se fasse de façon fluide et d’empêcher les appels accidentels du code Java qui pourraient violer les contraintes internes d’une classe inline. Poursuivez votre lecture ci-dessous pour savoir comment la rendre utilisable à partir de Java.

L’enveloppe (wrapper) n’est pas toujours éliminée dans le bytecode. Cela n’arrive que lorsque c’est possible et fonctionne de manière très similaire aux types primitifs intégrés. Lorsque vous définissez une variable du type Color ou que vous la passez directement dans une fonction, elle est remplacée par la valeur sous-jacente :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
val color = Color(0)        // primitive
changeBackground(color)     // primitive

Dans cet exemple, la variable color a le type Color lors de la compilation, mais elle est remplacée par Int dans le bytecode.

En revanche, si vous la stockez dans une collection ou si vous la passez dans une fonction générique, elle est encapsulée (boxing) dans un objet ordinaire du type Color :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
genericFunc(color)         // boxed
val list = listOf(color)   // boxied
val first = list.first()   // unboxed back to primitive

La conversion boxing et unboxing est effectuée automatiquement par le compilateur. Vous n’avez rien à faire, mais il est utile d’en comprendre le fonctionnement.

Nom : kotlin.png
Affichages : 12404
Taille : 155,9 Ko

Changement du nom de la JVM pour les appels Java

À partir de la version 1.4.30, vous pouvez changer le nom JVM d’une fonction prenant une classe inline comme paramètre pour la rendre utilisable depuis Java. Par défaut, ces noms sont modifiés pour éviter les utilisations accidentelles de Java ou les surcharges conflictuelles (comme changeBackground-euwHqFQ dans l’exemple ci-dessus).

Si vous annotez une fonction avec @JvmName, cela change le nom de cette fonction dans le bytecode et permet de l’appeler depuis Java et d’envoyer directement une valeur :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// déclarations Kotlin
inline class Timeout(val millis: Long)
 
val Int.millis get() = Timeout(this.toLong())
val Int.seconds get() = Timeout(this * 1000L)
 
@JvmName("greetAfterTimeoutMillis")
fun greetAfterTimeout(timeout: Timeout)
 
// utilisation Kotlin
greetAfterTimeout(2.seconds)
 
// utilisation Java
greetAfterTimeoutMillis(2000);

Comme toujours avec une fonction annotée avec @JvmName, depuis Kotlin vous l’appelez par son nom Kotlin. Kotlin offre un typage sûr car vous pouvez seulement envoyer une valeur de type Timeout en tant qu’argument et les unités sont évidentes dans ce contexte.

À partir de Java, vous pouvez envoyer directement une valeur long. Cela n’empêche plus les erreurs de types et c’est pourquoi cela ne fonctionne pas par défaut. Si vous voyez greetAfterTimeout(2) dans le code, il n’est pas immédiatement évident de savoir s’il s’agit de 2 secondes, 2 millisecondes ou 2 ans.

En fournissant l’annotation, vous soulignez explicitement votre intention d’appeler cette fonction depuis Java. Un nom descriptif permet d’éviter toute confusion : l’ajout du suffixe « Millis » au nom de la JVM rend les unités claires pour les utilisateurs de Java.

Blocs init

Autre amélioration pour les classes inline dans la version 1.4.30 : vous pouvez maintenant définir la logique d’initialisation dans le bloc init :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
inline class Name(val s: String) {
   init {
       require(s.isNotEmpty())
   }
}

C’était interdit auparavant.

Classes de valeurs inline

Cette version stabilise le concept de classes inline et l’intègre à une fonctionnalité plus générale : les classes de valeurs.

Jusqu’à présent, les classes « inline » constituaient une fonctionnalité de langage distincte. Elles constituent maintenant une optimisation spécifique de la JVM pour une classe de valeur avec un seul paramètre. Les classes de valeurs représentent un concept plus général et prendront en charge plusieurs optimisations : les classes inline aujourd’hui et plus tard les classes primitives Valhalla lorsque le projet Valhalla sera disponible.

La seule chose qui change pour vous pour le moment est la syntaxe. Étant donné qu’une classe inline est une classe de valeur optimisée, vous devez la déclarer différemment :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
@JvmInline
value class Color(val rgb: Int)

Vous définissez une classe de valeurs avec un paramètre de constructeur et l’annotez avec @JvmInline. JetBrains invite les développeurs à utiliser cette nouvelle syntaxe à partir de Kotlin 1.5. L’ancienne syntaxe inline class continuera à fonctionner pendant un certain temps. Un avertissement dans la 1.5 vous informera de son abandon et comprendra une option de migration automatique de toutes vos déclarations. Par la suite, elle sera obsolète et renverra une erreur.

Classes de valeurs

Une classe value représente une entité immuable avec des données. Actuellement, une classe value ne peut contenir qu’une seule propriété pour prendre en charge le cas d’utilisation des « anciennes » classes inline.

Dans les futures versions de Kotlin qui prendront en charge cette fonctionnalité, il sera possible de définir des classes de valeurs avec plusieurs propriétés. Toutes les valeurs doivent être des val en lecture seule :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
value class Point(val x: Int, val y: Int)

Les classes de valeurs n’ont pas d’identité : elles sont entièrement définies par les données stockées et les contrôles d’identité === ne sont pas autorisés pour elles. Le contrôle d’égalité == compare automatiquement les données sous-jacentes.

Cette qualité « sans identité » des classes de valeur permettra d’importantes optimisations par la suite : l’arrivée du projet Valhalla dans la JVM permettra d’implémenter des classes de valeur en tant que classes primitives de la JVM en coulisse.

La contrainte d’immuabilité, et donc la possibilité d’optimisations Valhalla, différencie les classes value des classes data.

Futures optimisations avec Valhalla

Le project Valhalla introduit un nouveau concept dans Java et dans la JVM : les classes primitives.

L’objectif principal des classes primitives est de combiner des primitives performantes avec les avantages orientés objet des classes JVM ordinaires. Les classes primitives sont des conteneurs de données dont les instances peuvent être stockées dans des variables, sur la pile de calcul, et exploitées directement, sans en-têtes ni pointeurs. À cet égard, elles sont similaires aux valeurs primitives comme int, long, etc. (en Kotlin, on ne travaille pas directement avec les types primitifs mais le compilateur les génère en coulisse).

Les classes primitives présentent l’avantage notable de permettre la disposition plane et dense des objets en mémoire. Actuellement, Array<Point> est un tableau de références. Avec la prise en charge de Valhalla, lorsque l’on définit Point comme une classe primitive (dans la terminologie Java) ou comme une classe de valeur avec l’optimisation sous-jacente (dans la terminologie Kotlin), la JVM peut l’optimiser et stocker un tableau de Points dans une disposition « plane », directement sous la forme d’un tableau de plusieurs x et y plutôt qu’un tableau de références.

JetBrains dit attendre avec impatience les changements à venir dans la JVM et vouloir que Kotlin en bénéficie. Pour autant, l'éditeur ne veut pas forcer sa communauté à dépendre des nouvelles versions de la JVM pour utiliser les classes de valeurs, il va donc les prendre en charge également pour les versions antérieures de la JVM. Lors de la compilation du code de la JVM avec le prise en charge de Valhalla, les dernières optimisations de la JVM fonctionneront pour les classes de valeurs.

Méthodes de mutation

Il y a encore beaucoup à dire sur la fonctionnalité de classes de valeurs. Comme les classes de valeurs représentent des données « immuables », des méthodes de mutation, comme celles de Swift, sont possibles pour elles. Une méthode de mutation est utilisée lorsqu’une fonction membre ou un setter de propriété renvoie une nouvelle instance plutôt que de mettre à jour une instance existante. Leur principal avantage est que vous les utilisez avec une syntaxe familière. Ce point doit encore être prototypé dans le langage.

Plus d’informations

L’annotation @JvmInline est spécifique à la JVM. Les classes de valeurs peuvent être implémentées différemment sur d’autres backends. Par exemple, sous forme de structs Swift dans Kotlin/Native.

Prise en charge pour les enregistrements de la JVM

Autre amélioration à venir dans l’écosystème de la JVM : les enregistrements Java. Ils sont analogues aux classes data de Kotlin et sont principalement de simples conteneurs de données.

Les enregistrements Java ne suivent pas la convention JavaBeans et ont des méthodes « x() » et « y() » au lieu des méthodes familières « getX() » et « getY() ».

L’interopérabilité avec Java a toujours été et reste une priorité pour Kotlin. Ainsi, le code Kotlin « comprend » les nouveaux enregistrements Java et les considère comme des classes ayant des propriétés Kotlin. Cela fonctionne comme pour les classes Java normales suivant la convention JavaBeans :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
// Java
record Point(int x, int y) { }
// Kotlin
fun foo(point: Point) {
    point.x // seen as property
    point.x() // also works
}

Principalement pour des raisons d’interopérabilité, vous pouvez annoter votre classe data avec @JvmRecord pour que de nouvelles méthodes d’enregistrement JVM soient générées :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
@JvmRecord
data class Point(val x: Int, val y: Int)

L’annotation @JvmRecord permet au compilateur de générer les méthodes x() et y() au lieu des méthodes standard getX() et getY(). Nous supposons qu’il vous suffit d’utiliser cette annotation pour préserver l’API de la classe lors de la conversion de Java en Kotlin. Dans tous les autres cas d’utilisation, les classes data familières de Kotlin peuvent s’utiliser à la place sans problème.

Cette annotation n’est disponible que si vous compilez le code Kotlin vers une version 15+ de la JVM.

Améliorations des interfaces et des classes scellées

Lorsque vous déclarez une classe sealed, cela restreint la hiérarchie à des sous-classes définies, ce qui permet des vérifications exhaustives dans les branches when. Dans Kotlin 1.4, la hiérarchie des classes scellées comporte deux contraintes. Premièrement, la classe supérieure ne peut pas être une interface scellée, ce doit être une classe. Deuxièmement, toutes les sous-classes doivent se trouver dans le même fichier.

Kotlin 1.5 supprime ces deux contraintes : vous pouvez désormais créer une interface scellée. Les sous-classes (tant pour les classes scellées que pour les interfaces scellées) doivent se trouver dans la même unité de compilation et dans le même paquet que la super classe, mais elles peuvent maintenant se trouver dans des fichiers différents.

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
sealed interface Expr
data class Const(val number: Double) : Expr
data class Sum(val e1: Expr, val e2: Expr) : Expr
object NotANumber : Expr
 
fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

Les classes scellées, et maintenant les interfaces scellées, sont utiles pour définir des hiérarchies de types de données abstraites (ADT).

Un autre cas d'utilisation important qui peut maintenant être bien traité avec des interfaces scellées est la fermeture d'une interface pour l'héritage et l'implémentation en dehors de la bibliothèque. Définir une interface comme sealed restreint son implémentation à la même unité de compilation et au même paquet, ce qui, dans le cas d’une bibliothèque, rend impossible son implémentation en dehors de la bibliothèque.

Par exemple, l’interface Job du paquet kotlinx.coroutines est uniquement destinée à être implémentée à l’intérieur de la bibliothèque kotlinx.coroutines. Le fait de la rendre sealed rend cette intention explicite :

Code Kotlin : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
package kotlinx.coroutines
sealed interface Job { ... }

En tant qu’utilisateur de la bibliothèque, vous n’êtes plus autorisé à définir votre propre sous-classe de Job. Cela a toujours été « implicite », mais avec les interfaces scellées, le compilateur peut formellement l’interdire.

Utiliser la prise en charge de la JVM à l’avenir

La prise en charge de l’aperçu des classes scellées a été inaugurée dans la version Java 15 et sur la JVM. À l’avenir, JetBrains va utiliser la prise en charge naturelle de la JVM pour les classes scellées si vous compilez le code Kotlin avec la dernière JVM (très probablement la JVM 17 ou une version ultérieure lorsque cette fonctionnalité sera stable).

En Java, vous listez explicitement toutes les sous-classes de la classe ou de l’interface scellée en question :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
// Java
public sealed interface Expression
    permits Const, Sum, NotANumber { ... }

Ces informations sont stockées dans le fichier de la classe à l’aide du nouvel attribut PermittedSubclasses. La JVM reconnaît les classes scellées au moment de l’exécution et empêche leur extension par sous-classes non autorisées.

À l’avenir, lorsque vous compilerez Kotlin sur la dernière JVM, cette nouvelle prise en charge de JVM pour les classes scellées sera utilisée. En coulisse, le compilateur va générer une liste de sous-classes autorisées dans le bytecode pour s’assurer que la JVM est prise en charge et procéder à des vérifications supplémentaires lors de l’exécution.

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
// pour la JVM 17 or later
Expr::class.java.permittedSubclasses // [Const, Sum, NotANumber]

En Kotlin, vous n’avez pas besoin de spécifier la liste de sous-classes ! Le compilateur générera la liste en fonction des sous-classes déclarées dans le même paquet.

La possibilité de spécifier explicitement les sous-classes pour une super classe ou une interface pourrait être ajoutée ultérieurement comme spécification optionnelle. Pour le moment, JetBrains pense que cela ne sera pas nécessaire, mais préfère attendre de connaître vos cas d’utilisation pour savoir si vous avez besoin de cette fonctionnalité !

Veuillez noter que pour les anciennes versions de la JVM, il est théoriquement possible de définir une sous-classe Java pour l’interface scellée de Kotlin, mais ne le faites pas ! Étant donné que la prise en charge de la JVM pour les sous-classes autorisées n’est pas encore disponible, cette contrainte n’est appliquée que par le compilateur Kotlin. JetBrains ajoutera des avertissements dans l’EDI pour éviter que cela ne se produise accidentellement. Par la suite, le nouveau mécanisme sera utilisé pour les dernières versions de la JVM afin de vérifier l’absence de sous-classes « non autorisées » de Java.

Outils de build

Prise en charge du cache de configuration dans le plugin Kotlin Gradle

Avec Kotlin 1.4.30, the plugin Kotlin Gradle est compatible avec le cache de configuration Gradle. Cela accélère le processus de build. Par exemple, Square, qui utilise Kotlin pour Android, a un build (Android, Java, Kotlin) de 1 800 modules. Son équipe rapporte les chiffres suivants :
  • Le tout premier build a pris 16 minutes et 30 secondes.
  • La deuxième a été beaucoup plus rapide ; elle n’a demandé que 5 minutes 45 secondes.

Plus précisément, pour Square, le cache de configuration permet d’économiser 1 minute et 10 secondes de création de graphiques de tâches et de configuration par build.
Lorsque vous exécutez la commande, Gradle exécute la phase de configuration et calcule le graphique des tâches. Gradle met le résultat en cache et le réutilise pour les builds suivants, ce qui vous fait gagner du temps.

Kotlin/Native

Amélioration du temps de compilation

JetBrains a amélioré le temps de compilation dans la version 1.4.30. Le temps nécessaire pour reconstituer l’exemple de framework KMM Networking and Data Storage est passé de 9,5 secondes (avec la version 1.4.10) à 4,5 secondes (avec la version 1.4.30).

Nom : compilation.png
Affichages : 1409
Taille : 44,9 Ko

Prise en charge du simulateur WatchOS 64 bits

Avec la version 1.3.60 de Kotlin en octobre 2018, JetBrains inaugure la prise en charge de la création d’applications Kotlin pour des simulateurs Apple Watch. En novembre dernier, l’architecture du simulateur Apple Watch est passée de i386 à x86_64, ce qui a posé des problèmes aux développeurs travaillant sur cette fonctionnalité. La nouvelle cible Kotlin/Native watchosX64 permet d’exécuter le simulateur watchOS sur une architecture 64 bits et elle fonctionne sur watchOS à partir de la version 7.0.

Prise en charge du SDK Xcode 12.2

Kotlin/Native prend désormais en charge Xcode 12.2. Les frameworks macOS qui ont été ajoutés à la version Xcode 12.2 peuvent être utilisés avec cette mise à jour de Kotlin. Par exemple, le framework MLCompute est maintenant disponible pour les utilisateurs qui développent des applications pour MacOS.

Bibliothèque standard

API indépendante des paramètres de localisation pour les textes en majuscules et minuscules

Cette version inaugure une API expérimentale indépendante des paramètres de localisation pour changer la casse des chaînes et des caractères. Les fonctions actuelles toLowerCase(), toUpperCase(), capitalize(), decapitalize() de l’API sont sensibles aux paramètres locaux, ce qui n’est pas évident et peu pratique dans certains cas. Des paramètres locaux différents sur la plateforme affectent le comportement du code : par exemple, en langue turque, lorsque la chaîne « kotlin » est convertie par toUpperCase, cela donne « KOTLİN » et non « KOTLIN ». Le langage utilise maintenant les paramètres locaux de la racine.

API non ambiguë pour la conversion des caractères

Les fonctions actuelles de conversion de « Char » en nombres, qui renvoient son code UTF-16 exprimé en différents types numériques, sont souvent confondues avec la conversion similaire String-to-Int, qui renvoie la valeur numérique d’une chaîne de caractères.

Pour éviter cette confusion, Jetrains a décidé de séparer les conversions de caractères en deux ensembles de fonctions clairement nommées : les fonctions permettant d’obtenir le code de l’entier de Char et de construire Char et les fonctions permettant de convertir Char en la valeur numérique du nombre qu’il représente.

Source : JetBrains