Kotlin
A concise multiplatform language developed by JetBrains
Vorschau der neuen Sprachmerkmale in Kotlin 1.4.30
Wir planen, in Kotlin 1.5 neue Sprachmerkmale hinzuzufügen, und Sie können diese bereits in Kotlin 1.4.30 ausprobieren:
- Stabilisierung von Inline-Wertklassen
- Experimentelle Unterstützung für JVM-Records
- Experimentelle Unterstützung für Sealed-Interfaces und weitere Verbesserungen für Sealed-Klassen
Um diese neuen Funktionen auszuprobieren, müssen Sie die Sprachversion 1.5 angeben.
Der neue Releasezyklus sorgt dafür, dass Kotlin 1.5 erst in ein paar Monaten veröffentlicht wird, aber die neuen Funktionen sind bereits in 1.4.30 als Vorschau verfügbar. Ihr frühzeitiges Feedback ist für uns sehr wichtig, also probieren Sie diese neuen Funktionen bitte jetzt aus!
Stabilisierung von Inline-Wertklassen
Inline-Klassen sind seit Kotlin 1.3 in der Alphaphase verfügbar, und in 1.4.30 werden sie in die Betaphase befördert.
Kotlin 1.5 stabilisiert das Konzept der Inline-Klassen, macht es aber zu einem Teil einer allgemeineren Funktion, den Value-Klassen, die wir später in diesem Beitrag beschreiben werden.
Wir beginnen mit einer Auffrischung, wie Inline-Klassen funktionieren. Wenn Sie mit Inline-Klassen bereits vertraut sind, können Sie diesen Abschnitt überspringen und direkt zu den neuen Änderungen übergehen.
Zur Erinnerung, eine Inline-Klasse eliminiert einen Wrapper um einen Wert:
Eine Inline-Klasse kann sowohl ein Wrapper für einen primitiven Typ als auch für einen beliebigen Referenztyp, wie String
, sein.
Der Compiler ersetzt Inline-Klasseninstanzen (in unserem Beispiel die Color
-Instanz) im Bytecode durch den zugrunde liegenden Typ (Int
), wenn dies möglich ist:
Im Hintergrund erzeugt der Compiler die Funktion changeBackground
mit einem beschädigten Namen, der Int
als Parameter nimmt, und er übergibt die Konstante 255
direkt, ohne einen Wrapper an der Aufrufstelle zu erzeugen:
Der Name wird beschädigt, um die nahtlose Überladung von Funktionen zu ermöglichen, die Instanzen verschiedener Inline-Klassen annehmen, und um versehentliche Aufrufe aus dem Java-Code zu verhindern, die die internen Beschränkungen einer Inline-Klasse verletzen könnten. Lesen Sie unten weiter, wie Sie ihn von Java aus nutzbar machen können.
Der Wrapper wird nicht immer im Bytecode eliminiert. Dies geschieht nur, wenn es möglich ist, und es funktioniert ganz ähnlich wie bei eingebauten primitiven Typen. Wenn Sie eine Variable vom Typ Color
definieren oder sie direkt an eine Funktion übergeben, wird sie durch den zugrunde liegenden Wert ersetzt:
In diesem Beispiel hat die Color
-Variable bei der Kompilierung den Typ Color
, im Bytecode wird sie aber durch Int
ersetzt.
Wenn Sie sie jedoch in einer Sammlung speichern oder an eine generische Funktion übergeben, wird sie in ein reguläres Objekt vom Typ Color
geschachtelt:
Das Boxing und Unboxing wird automatisch vom Compiler durchgeführt. Sie müssen nichts dagegen tun, aber es ist nützlich, die Interna zu verstehen.
Ändern des JVM-Namens für Java-Aufrufe
Ab 1.4.30 können Sie den JVM-Namen einer Funktion ändern, die eine Inline-Klasse als Parameter annimmt, um sie von Java aus nutzbar zu machen. Standardmäßig werden solche Namen gemangelt, um versehentliche Verwendungen aus Java oder widersprüchliche Überladungen zu verhindern (wie changeBackground-euwHqFQ
im obigen Beispiel).
Wenn Sie eine Funktion mit @JvmName
annotieren, ändert dies den Namen dieser Funktion im Bytecode und ermöglicht es, sie von Java aus aufzurufen und direkt einen Wert zu übergeben:
Wie immer bei einer mit @JvmName
annotierten Funktion, rufen Sie diese von Kotlin aus mit ihrem Kotlin-Namen auf. Die Verwendung in Kotlin ist typsicher, da Sie nur einen Wert vom Typ Timeout
als Argument übergeben können, und die Einheiten sind aus der Verwendung ersichtlich.
Von Java aus können Sie einen Long
-Wert direkt übergeben. Das ist nicht mehr typsicher, und deshalb funktioniert es standardmäßig auch nicht. Wenn Sie greetAfterTimeout(2)
im Code sehen, ist es nicht sofort klar, ob es sich um 2 Sekunden, 2 Millisekunden oder 2 Jahre handelt.
Mit der Annotation betonen Sie explizit, dass diese Funktion von Java aus aufgerufen werden soll. Ein beschreibender Name hilft, Verwechslungen zu vermeiden: Das Hinzufügen des Suffixes “Millis” zum JVM-Namen macht die Einheiten für Java-Benutzer*innen deutlich.
Init-Blöcke
Eine weitere Verbesserung für Inline-Klassen in 1.4.30 ist, dass Sie nun Initialisierungslogik im Init
-Block definieren können:
Dies war bisher nicht erlaubt.
Weitere Details zu Inline-Klassen können Sie im entsprechenden KEEP, in der Dokumentation und in der Diskussion unter diesem Issue nachlesen.
Inline-Wertklassen
Kotlin 1.5 stabilisiert das Konzept der Inline-Klassen und macht es zu einem Teil einer allgemeineren Funktion: Value-Klassen.
Bisher stellten “Inline”-Klassen ein separates Sprachmerkmal dar, aber sie werden nun zu einer spezifischen JVM-Optimierung für eine Wertklasse mit einem Parameter. Wertklassen stellen ein allgemeineres Konzept dar und werden verschiedene Optimierungen unterstützen: Inline-Klassen jetzt und primitive Valhalla-Klassen in der Zukunft, wenn das Projekt Valhalla verfügbar wird (mehr dazu unten).
Das einzige, was sich für Sie im Moment ändert, ist die Syntax. Da eine Inline-Klasse eine optimierte Wertklasse ist, müssen Sie sie anders deklarieren als bisher:
Sie definieren eine Wertklasse mit einem Konstruktorparameter und annotieren sie mit @JvmInline
. Wir erwarten, dass jeder diese neue Syntax ab Kotlin 1.5 verwenden wird. Die alte Syntax Inline-Klasse
wird noch eine Zeit lang funktionieren. Sie wird in 1.5 mit einer Warnung veraltet sein und eine Option enthalten, um alle Ihre Deklarationen automatisch zu migrieren. Später wird sie mit einem Fehler verworfen.
Wertklassen
Eine value-
Klasse repräsentiert eine unveränderliche Entität mit Daten. Im Moment kann eine value-
Klasse nur eine Eigenschaft enthalten, um den Anwendungsfall der “alten” Inline-Klassen zu unterstützen.
In zukünftigen Kotlin-Versionen mit voller Unterstützung für diese Funktion wird es möglich sein, Wertklassen mit vielen Eigenschaften zu definieren. Alle Werte sollten schreibgeschützte val
s sein:
Werteklassen haben keine Identität: Sie sind vollständig durch die gespeicherten Daten definiert und === Identitätsprüfungen sind für sie nicht erlaubt. Die == Gleichheitsprüfung vergleicht automatisch die zugrunde liegenden Daten.
Diese “identitätslose” Eigenschaft von Wertklassen ermöglicht bedeutende zukünftige Optimierungen: Mit der Ankunft von Project Valhalla in der JVM können Wertklassen im Hintergrund als JVM-Primitivklassen implementiert werden.
Die Unveränderlichkeitsbeschränkung und damit die Möglichkeit von Valhalla-Optimierungen unterscheidet value
-Klassen von data
-Klassen.
Zukünftige Valhalla-Optimierung
Das Projekt Valhalla führt ein neues Konzept in Java und der JVM ein: primitive Klassen.
Das Hauptziel von primitiven Klassen ist es, performante Primitive mit den objektorientierten Vorteilen von regulären JVM-Klassen zu kombinieren. Primitive Klassen sind Datenhalter, deren Instanzen in Variablen oder auf dem Berechnungs-Stack gespeichert werden können und auf die direkt, ohne Header und Pointer, zugegriffen werden kann. In dieser Hinsicht ähneln sie primitiven Werten wie int
, long
, etc. (in Kotlin arbeitet man nicht direkt mit primitiven Typen, sondern der Compiler erzeugt sie im Hintergrund).
Ein wichtiger Vorteil von primitiven Klassen ist, dass sie eine flache und dichte Anordnung von Objekten im Arbeitsspeicher ermöglichen. Derzeit ist Array<Point>
ein Array von Referenzen. Mit der Valhalla-Unterstützung kann die JVM bei der Definition von Point
als primitive Klasse (in Java-Terminologie) oder als Wertklasse mit der zugrundeliegenden Optimierung (in Kotlin-Terminologie) diese optimieren und ein Array von Point
s in einem “flachen” Layout speichern, als Array von vielen x
s und y
s direkt, nicht als Array von Referenzen.
Wir freuen uns sehr auf die kommenden JVM-Änderungen und wir wollen, dass Kotlin davon profitiert. Gleichzeitig wollen wir unsere Community nicht zwingen, von neuen JVM-Versionen abhängig zu sein, um Value-Klassen
verwenden zu können, und deshalb werden wir sie auch für frühere JVM-Versionen unterstützen. Beim Kompilieren des Codes für die JVM mit Valhalla-Unterstützung werden die neuesten JVM-Optimierungen für Value-Klassen funktionieren.
Mutierende Methoden
Es gibt noch viel mehr über die Funktionalität von Wertklassen zu sagen. Da Wertklassen “unveränderliche” Daten darstellen, sind für sie mutierende Methoden, wie in Swift, möglich. Von einer mutierenden Methode spricht man, wenn eine Member-Funktion oder ein Property-Setter eine neue Instanz zurückgibt, anstatt eine bestehende zu aktualisieren. Der Hauptvorteil ist, dass man sie mit einer vertrauten Syntax verwenden kann. Dies muss noch in der Sprache prototypisiert werden.
Weitere Details
Die Annotation @JvmInline
ist JVM-spezifisch. Auf anderen Backends können Wertklassen anders implementiert werden. Zum Beispiel als Swift-Strukturen in Kotlin/Native.
Die Details zu Wertklassen können Sie in der Design Note für Kotlin-Wertklassen nachlesen oder sich einen Auszug aus dem Vortrag “Ein Blick in die Zukunft” von Roman Elizarov ansehen.
Unterstützung für JVM-Records
Eine weitere kommende Verbesserung im JVM-Ökosystem sind Java-Records. Sie sind analog zu den Kotlin-Datenklassen
und werden hauptsächlich als einfache Datenhalter verwendet.
Java records don’t follow the JavaBeans convention, and they have x()
and y()
methods instead of the familiar getX()
and getY()
.
Interoperabilität mit Java war schon immer eine Priorität für Kotlin und wird es auch in Zukunft sein. So “versteht” der Kotlin-Code neue Java-Records und sieht sie als Klassen mit Kotlin-Eigenschaften. Dies funktioniert wie bei regulären Java-Klassen, die der JavaBeans-Konvention folgen:
Vor allem aus Gründen der Interoperabilität können Sie Ihre data
-Klasse mit @JvmRecord
annotieren, um neue JVM-Record-Methoden generieren zu lassen:
Die Annotation @JvmRecord
veranlasst den Compiler, x()
– und y()
-Methoden anstelle der Standardmethoden getX()
und getY()
zu generieren. Wir gehen davon aus, dass Sie diese Annotation nur verwenden müssen, um die API der Klasse bei der Konvertierung von Java nach Kotlin zu erhalten. In allen anderen Anwendungsfällen können stattdessen die bekannten data
-Klassen von Kotlin ohne Probleme verwendet werden.
Diese Annotation ist nur verfügbar, wenn Sie Kotlin-Code bis zur Version 15+ von JVM kompilieren. Sie können mehr über diese Funktion im entsprechenden KEEP oder in der Dokumentation lesen, sowie in der Diskussion in diesem Issue.
Verbesserungen bei Sealed-Interfaces und Sealed-Klassen
Wenn Sie eine Klasse ‘sealed’ machen, schränkt dies die Hierarchie auf definierte Unterklassen ein, was lückenlos Prüfungen von when
Branches ermöglicht. In Kotlin 1.4 wird die Sealed-Klassenhierarchie mit zwei Einschränkungen versehen. Erstens kann die oberste Klasse keine Sealed-Interface sein, sondern muss eine Klasse sein. Zweitens sollten sich alle Unterklassen in der gleichen Datei befinden.
Kotlin 1.5 hebt beide Beschränkungen auf: Sie können jetzt ein Interface ‘sealed’ machen. Die Unterklassen (sowohl zu Sealed-Klassen als auch zu Sealed-Interfaces) sollten sich in der gleichen Kompilierungseinheit und im gleichen Paket wie die Oberklasse befinden, aber sie können jetzt in verschiedenen Dateien liegen.
Sealed-Klassen und jetzt auch Interfaces sind nützlich, um abstrakte Datentyphierarchien (ADT) zu definieren.
Ein weiterer wichtiger Anwendungsfall, der jetzt mit Sealed-Interfaces gut adressiert werden kann, ist das Schließen eines Interfaces für die Vererbung and Implementierung außerhalb der Bibliothek. Die Definition eines Interfaces als ‘sealed’ schränkt ihre Implementierung auf dieselbe Kompilierungseinheit und dasselbe Paket ein, was im Fall einer Bibliothek eine Implementierung außerhalb der Bibliothek unmöglich macht.
Das Interface Job
aus dem Paket kotlinx.coroutines
ist beispielsweise nur für die Implementierung innerhalb der Bibliothek kotlinx.coroutines
vorgesehen. Dadurch, dass sie jetzt sealed
ist, wird diese Intention explizit gemacht:
Als Benutzer der Bibliothek ist es Ihnen nicht mehr erlaubt, eine eigene Unterklasse von Job
zu definieren. Dies war immer “implizit”, aber mit Sealed-Interfaces kann der Compiler dies formal verbieten.
Zukünftige Verwendung der JVM-Unterstützung
Die Unterstützung für Sealed-Klassen wurde in Java 15 und auf der JVM eingeführt. In Zukunft werden wir die natürliche JVM-Unterstützung für Sealed-Klassen nutzen, wenn Sie Kotlin-Code auf der neuesten JVM kompilieren (höchstwahrscheinlich JVM 17 oder später, wenn diese Funktion stabil wird).
In Java listen Sie explizit alle Unterklassen der gegebenen Sealed-Klasse oder -Interface auf:
Diese Information wird in der Klassendatei mit dem neuen Attribut PermittedSubclasses
gespeichert. Die JVM erkennt Sealed-Klassen zur Laufzeit und verhindert deren Erweiterung durch nicht erlaubte Unterklassen.
Wenn Sie Kotlin in der Zukunft in die neueste JVM kompilieren, wird diese neue JVM-Unterstützung für Sealed-Klassen verwendet. Im Hintergrund wird der Compiler eine Liste der erlaubten Unterklassen im Bytecode generieren, um sicherzustellen, dass die JVM-Unterstützung und zusätzliche Laufzeitprüfungen vorhanden sind.
In Kotlin müssen Sie die Liste der Unterklassen nicht angeben! Der Compiler generiert die Liste basierend auf den deklarierten Unterklassen im gleichen Paket.
Die Möglichkeit, die Unterklassen für eine Superklasse oder ein Interface explizit anzugeben, könnte später als optionale Angabe hinzugefügt werden. Im Moment vermuten wir, dass es nicht notwendig sein wird, aber wir würden uns freuen, von Ihren Anwendungsfällen zu hören, und ob Sie diese Funktionalität benötigen!
Beachten Sie, dass es für ältere JVM-Versionen theoretisch möglich ist, eine Java-Subklasse zur Kotlin-Sealed-Interface zu definieren, aber tun Sie es nicht! Da die JVM-Unterstützung für erlaubte Unterklassen noch nicht verfügbar ist, wird diese Einschränkung nur durch den Kotlin-Compiler erzwungen. Wir werden IDE-Warnungen hinzufügen, um zu verhindern, dass dies versehentlich geschieht. In Zukunft wird der neue Mechanismus für die neuesten JVM-Versionen verwendet werden, um sicherzustellen, dass es keine “unerlaubten” Unterklassen von Java gibt.
Mehr über Sealed-Interfaces und die gelockerten Einschränkungen für Sealed-Klassen können Sie im entsprechenden KEEP oder in der Dokumentation nachlesen, sowie die Diskussion in diesem Issue verfolgen.
Wie Sie die neuen Funktionen ausprobieren können
Sie müssen Kotlin 1.4.30 verwenden. Geben Sie die Sprachversion 1.5 an, um die neuen Funktionen zu aktivieren:
Um JVM-Records auszuprobieren, müssen Sie zusätzlich jvmTarget 15 verwenden und die JVM-Vorschau-Funktionen aktivieren: Fügen Sie die Compiler-Optionen -language-version 1.5
und –Xjvm-enable-preview
hinzu.
Pre-Release-Hinweise
Beachten Sie, dass die Unterstützung für die neuen Funktionen experimentell ist und die Unterstützung der Sprachversion 1.5 sich im Pre-Release-Status befindet. Die Sprachversion auf 1.5 im Kotlin 1.4.30 Compiler einzustellen ist vergleichbar mit der 1.5-M0-Vorschau auszuprobieren. Die Abwärtskompatibilitätsgarantien gelten nicht für Pre-Release-Versionen. Die Funktionen und die API können sich in nachfolgenden Versionen ändern. Wenn wir eine endgültige Kotlin 1.5-RC erreichen, werden alle Binärdateien, die mit Pre-Release-Versionen erzeugt wurden, vom Compiler geächtet und Sie müssen alles, was mit 1.5-Mx kompiliert wurde, neu kompilieren.
Geben Sie uns Feedback
Bitte probieren Sie die in diesem Beitrag beschriebenen neuen Funktionen aus und geben Sie uns Ihr Feedback! Sie können weitere Details in KEEPs finden und sich an den Diskussionen in YouTrack beteiligen. Sie können auch neue Probleme melden, wenn etwas bei Ihnen nicht funktioniert. Bitte teilen Sie Ihre Erkenntnisse darüber mit, wie gut die neuen Funktionen Anwendungsfälle in Ihren Projekten adressieren!
Weitere Lektüre und Diskussionen:
- Dokumentationsseite “Was ist neu”.
- Inline-Klassen KEEP; Diskussion: KT-42434.
- Design-Dokument für Wertklassen; Diskussion in KEEP.
- JVM-Records KEEP; Diskussion: KT-42430.
- Sealed-Interfaces und Sealed-Klassen KEEP; Diskussion: KT-42433.