Présentation des nouvelles fonctionnalités de langage dans Kotlin 1.4.30

Read this post in other languages:
English, 한국어, Deutsch, Português do Brasil, Русский, Español, 简体中文

Voici les nouvelles fonctionnalités de langage que nous prévoyons d’ajouter dans Kotlin 1.5 et que vous pouvez d’ores et déjà essayer dans Kotlin 1.4.30 :

Pour essayer ces nouvelles fonctionnalités, vous devez spécifier la version 1.5 du langage explicitement dans le code.

La nouvelle cadence de publication implique que Kotlin 1.5 ne sera disponible que dans quelques mois, mais de nouvelles fonctionnalités sont déjà disponibles en avant-première dans la version 1.4.30. Il est important pour nous de recevoir vos retours d’expérience au plus tôt, alors n’hésitez pas à essayer ces nouvelles fonctionnalités dès maintenant !

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 :

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 :

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 :

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 :

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 :

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 :

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 :

C’était interdit auparavant.

Vous pouvez en savoir plus sur les classes inline dans le KEEP correspondant, dans la documentation et dans la discussion de ce ticket.

Classes de valeurs inline

Kotlin 1.5 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 (voir ci-dessous pour en savoir plus).

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 :

Vous définissez une classe de valeurs avec un paramètre de constructeur et l’annotez avec @JvmInline. Nous vous invitons tous à 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 :

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.

Nous attendons avec impatience les changements à venir dans la JVM et nous voulons que Kotlin en bénéficie. Pour autant, nous ne voulons pas forcer notre communauté à dépendre des nouvelles versions de la JVM pour utiliser les classes de valeurs, nous allons 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.

Vous pouvez en lire plus sur les classes de valeurs dans la Note de conception pour les classes Kotlin ou regarder un extrait de l’intervention de Roman Elizarov « A look into the future ».

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 :

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 :

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. Vous pouvez en savoir plus sur cette fonctionnalité dans le KEEP correspondant, dans la documentation et dans la discussion de ce ticket.

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.

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).

Another important use case that can now be nicely addressed with sealed interfaces is closing an interface for inheritance and implementation outside the library. 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 :

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, nous allons 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 :

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.

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, nous pensons que cela ne sera pas nécessaire, mais nous attendons 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. Nous ajouterons des avertissements dans l’IDE 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.

Vous pouvez en savoir plus sur les interfaces scellées et les restrictions relâchées des classes scellées dans le KEEP correspondant ou dans la documentation et consulter la discussion dans ce ticket.

Comment essayer les nouvelles fonctionnalités

Vous devez utiliser Kotlin 1.4.30. Spécifiez la version 1.5 du langage pour activer les nouvelles fonctionnalités :

Pour essayer les enregistrements JVM, vous devez en outre utiliser jvmTarget 15 et activer les fonctions d’aperçu de la JVM : ajoutez les options de compilation -language-version 1.5 et -Xjvm-enable-preview.

Notes de pré-version

Notez que la prise en charge des nouvelles fonctionnalités est expérimentale et que la prise en charge de la version 1.5 du langage est en phase de pré-publication. Paramétrer la version du langage en 1.5 dans le compilateur Kotlin 1.4.30 équivaut à essayer la preview 1.5-M0. Les garanties de rétrocompatibilité ne couvrent pas les versions en pré-publication. Les fonctionnalités et l’API peuvent changer dans les versions ultérieures. Lorsque nous arriverons à la version finale Kotlin 1.5-RC, tous les binaires produits par les versions de pré-publication seront interdits par le compilateur et vous devrez recompiler tout le code compilé par 1.5-Mx.

Faites-nous part de vos commentaires

Essayez les nouvelles fonctionnalités décrites dans ce post et partagez vos retours avec nous ! Vous pouvez trouver plus de détails dans les KEEP, participer aux discussions dans YouTrack et nous signaler d’éventuels incidents. Faites-nous part de vos observations sur l’efficacité des nouvelles fonctionnalités pour les cas d’utilisation de vos projets !

Lectures et discussions complémentaires :

Discover more