
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 |
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 |
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 |
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 |
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 |
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.
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 à...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.