Kotlin-Code aus Java aufrufen

In diesem Codelab erfahren Sie, wie Sie Ihren Kotlin-Code so schreiben oder anpassen, dass er sich nahtloser aus Java-Code aufrufen lässt.

Lerninhalte

  • So nutzen Sie @JvmField, @JvmStatic und andere Anmerkungen.
  • Einschränkungen beim Zugriff auf bestimmte Kotlin-Sprachfunktionen aus Java-Code.

Was Sie bereits wissen müssen

Dieses Codelab richtet sich an Programmierer und setzt grundlegende Kenntnisse in Java und Kotlin voraus.

In diesem Codelab wird die Migration eines Teils eines größeren Projekts simuliert, das in der Programmiersprache Java geschrieben wurde, um neuen Kotlin-Code einzubinden.

Zur Vereinfachung verwenden wir eine einzelne .java-Datei mit dem Namen UseCase.java, die den vorhandenen Code darstellt.

Wir stellen uns vor, dass wir gerade einige Funktionen, die ursprünglich in Java geschrieben wurden, durch eine neue Version in Kotlin ersetzt haben und die Integration abschließen müssen.

Projekt importieren

Der Code des Projekts kann hier aus dem GitHub-Projekt geklont werden: GitHub

Alternativ können Sie das Projekt aus einem ZIP-Archiv herunterladen und extrahieren, das Sie hier finden:

Zip herunterladen

Wenn Sie IntelliJ IDEA verwenden, wählen Sie „Import Project“ (Projekt importieren) aus.

Wenn Sie Android Studio verwenden, wählen Sie „Projekt importieren (Gradle, Eclipse ADT usw.)“ aus.

Öffnen wir UseCase.java und sehen wir uns die Fehler an.

Die erste Funktion mit einem Problem ist registerGuest:

public static User registerGuest(String name) {
   User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
   Repository.addUser(guest);
   return guest;
}

Die Fehler für Repository.getNextGuestId() und Repository.addUser(...) sind identisch: „Non-static cannot be accessed from a static context.“ (Auf nicht statische Elemente kann nicht über einen statischen Kontext zugegriffen werden.)

Sehen wir uns nun eine der Kotlin-Dateien an. Öffnen Sie die Datei Repository.kt.

Wir sehen, dass unser Repository ein Singleton ist, das mit dem Schlüsselwort „object“ deklariert wird. Das Problem ist, dass Kotlin eine statische Instanz innerhalb unserer Klasse generiert, anstatt diese als statische Attribute und Methoden verfügbar zu machen.

Repository.getNextGuestId() könnte beispielsweise mit Repository.INSTANCE.getNextGuestId() referenziert werden, aber es gibt eine bessere Methode.

Wir können Kotlin dazu bringen, statische Methoden und Attribute zu generieren, indem wir die öffentlichen Attribute und Methoden des Repository mit @JvmStatic annotieren:

object Repository {
   val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

Fügen Sie Ihrem Code mit Ihrer IDE die Annotation „@JvmStatic“ hinzu.

Wenn wir wieder zu UseCase.java wechseln, verursachen die Attribute und Methoden für Repository keine Fehler mehr, mit Ausnahme von Repository.BACKUP_PATH. Darauf kommen wir später noch einmal zurück.

Beheben wir jetzt den nächsten Fehler in der Methode „registerGuest()“.

Angenommen, wir haben eine StringUtils-Klasse mit mehreren statischen Funktionen für String-Operationen. Bei der Konvertierung in Kotlin haben wir die Methoden in Erweiterungsfunktionen konvertiert. In Java gibt es keine Erweiterungsfunktionen. Daher werden diese Methoden in Kotlin als statische Funktionen kompiliert.

Wenn wir uns die registerGuest()-Methode in UseCase.java ansehen, stellen wir leider fest, dass etwas nicht stimmt:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Der Grund dafür ist, dass Kotlin diese Funktionen auf oberster Ebene oder auf Paketebene in einer Klasse platziert, deren Name auf dem Dateinamen basiert. Da die Datei in diesem Fall „StringUtils.kt“ heißt, lautet der Name der entsprechenden Klasse StringUtilsKt.

Wir könnten alle Verweise auf StringUtils in StringUtilsKt ändern und diesen Fehler beheben. Das ist jedoch nicht ideal, weil:

  • Möglicherweise müssen viele Stellen in unserem Code aktualisiert werden.
  • Der Name selbst ist unpassend.

Anstatt unseren Java-Code umzugestalten, aktualisieren wir unseren Kotlin-Code, damit für diese Methoden ein anderer Name verwendet wird.

Öffnen Sie StringUtils.Kt und suchen Sie nach der folgenden Paketdeklaration:

package com.google.example.javafriendlykotlin

Wir können Kotlin anweisen, einen anderen Namen für die Methoden auf Paketebene zu verwenden, indem wir die @file:JvmName-Annotation verwenden. Wir verwenden diese Annotation, um die Klasse StringUtils zu benennen.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Wenn wir uns UseCase.java noch einmal ansehen, sehen wir, dass der Fehler für StringUtils.nameToLogin() behoben wurde.

Leider wurde dieser Fehler durch einen neuen Fehler ersetzt, der sich auf die Parameter bezieht, die an den Konstruktor für User übergeben werden. Fahren wir mit dem nächsten Schritt fort und beheben wir diesen letzten Fehler in UseCase.registerGuest().

Kotlin unterstützt Standardwerte für Parameter. Wir können sehen, wie sie verwendet werden, indem wir uns den init-Block von Repository.kt ansehen.

Repository.kt:

_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))

Für den Nutzer „warlow“ müssen wir keinen Wert für displayName angeben, da in User.kt ein Standardwert dafür festgelegt ist.

User.kt:

data class User(
   val id: Int,
   val username: String,
   val displayName: String = username.toTitleCase(),
   val groups: List<String> = listOf("guest")
)

Leider funktioniert das nicht auf dieselbe Weise, wenn die Methode über Java aufgerufen wird.

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Standardwerte werden in der Programmiersprache Java nicht unterstützt. Um dieses Problem zu beheben, weisen wir Kotlin mit der Annotation @JvmOverloads an, Überladungen für unseren Konstruktor zu generieren.

Zuerst müssen wir User.kt leicht aktualisieren.

Da die Klasse User nur einen einzigen primären Konstruktor hat und der Konstruktor keine Annotationen enthält, wurde das Schlüsselwort constructor weggelassen. Wenn wir sie nun annotieren möchten, muss das Schlüsselwort constructor enthalten sein:

data class User constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

Wenn das Keyword constructor vorhanden ist, können wir die Annotation @JvmOverloads hinzufügen:

data class User @JvmOverloads constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

Wenn wir wieder zu UseCase.java wechseln, sehen wir, dass in der Funktion registerGuest keine Fehler mehr vorhanden sind.

Als Nächstes beheben wir den fehlerhaften Aufruf von user.hasSystemAccess() in UseCase.getSystemUsers(). Fahren Sie mit dem nächsten Schritt fort oder lesen Sie weiter, um mehr darüber zu erfahren, was @JvmOverloads getan hat, um den Fehler zu beheben.

@JvmOverloads

Um besser zu verstehen, was @JvmOverloads macht, erstellen wir eine Testmethode in UseCase.java:

private void testJvmOverloads() {
   User syrinx = new User(1001, "syrinx");
   User ione = new User(1002, "ione", "Ione Saldana");

   List<String> groups = new ArrayList<>();
   groups.add("staff");
   User beaulieu = new User(1002, "beaulieu", groups);
}

Wir können ein User mit nur zwei Parametern erstellen: id und username:

User syrinx = new User(1001, "syrinx");

Wir können auch ein User erstellen, indem wir einen dritten Parameter für displayName einfügen und weiterhin den Standardwert für groups verwenden:

User ione = new User(1002, "ione", "Ione Saldana");

Es ist jedoch nicht möglich, displayName zu überspringen und nur einen Wert für groups anzugeben, ohne zusätzlichen Code zu schreiben:

Löschen wir also diese Zeile oder stellen wir „//“ voran, um sie auszukommentieren.

Wenn wir in Kotlin Standard- und Nicht-Standardparameter kombinieren möchten, müssen wir benannte Parameter verwenden.

// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))

Der Grund dafür ist, dass Kotlin Überladungen für Funktionen, einschließlich Konstruktoren, generiert, aber nur eine Überladung pro Parameter mit einem Standardwert erstellt.

Sehen wir uns noch einmal UseCase.java an und gehen wir das nächste Problem an: den Aufruf von user.hasSystemAccess() in der Methode UseCase.getSystemUsers():

public static List<User> getSystemUsers() {
   ArrayList<User> systemUsers = new ArrayList<>();
   for (User user : Repository.getUsers()) {
       if (user.hasSystemAccess()) {     // Now has an error!
           systemUsers.add(user);
       }
   }
   return systemUsers;
}

Das ist ein interessanter Fehler. Wenn Sie die Funktion zur automatischen Vervollständigung Ihrer IDE für die Klasse User verwenden, werden Sie feststellen, dass hasSystemAccess() in getHasSystemAccess() umbenannt wurde.

Um das Problem zu beheben, möchten wir, dass Kotlin einen anderen Namen für die val-Property hasSystemAccess generiert. Dazu können wir die Anmerkung @JvmName verwenden. Wechseln wir zurück zu User.kt und sehen wir uns an, wo wir sie anwenden sollten.

Es gibt zwei Möglichkeiten, die Anmerkung anzuwenden. Die erste Möglichkeit besteht darin, sie direkt auf die Methode get() anzuwenden:

val hasSystemAccess
   @JvmName("hasSystemAccess")
   get() = "sys" in groups

Dadurch wird Kotlin signalisiert, die Signatur des explizit definierten Getters in den angegebenen Namen zu ändern.

Alternativ können Sie es mit einem get:-Präfix auf die Property anwenden:

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

Die alternative Methode ist besonders nützlich für Properties, für die ein standardmäßiger, implizit definierter Getter verwendet wird. Beispiel:

@get:JvmName("isActive")
val active: Boolean

So kann der Name des Getters geändert werden, ohne dass ein Getter explizit definiert werden muss.

Trotz dieses Unterschieds können Sie die Formulierung verwenden, die Ihnen besser gefällt. In beiden Fällen wird in Kotlin ein Getter mit dem Namen hasSystemAccess() erstellt.

Wenn wir zu UseCase.java zurückkehren, können wir bestätigen, dass getSystemUsers() jetzt fehlerfrei ist.

Der nächste Fehler befindet sich in formatUser(). Wenn Sie mehr über die Namenskonvention für Kotlin-Getter erfahren möchten, lesen Sie hier weiter, bevor Sie mit dem nächsten Schritt fortfahren.

Benennung von Gettern und Settern

Wenn wir Kotlin schreiben, vergessen wir leicht, dass Code wie:

val myString = "Logged in as ${user.displayName}")

Ruft tatsächlich eine Funktion auf, um den Wert von displayName abzurufen. Wir können das überprüfen, indem wir im Menü zu Tools > Kotlin > Show Kotlin Bytecode gehen und dann auf die Schaltfläche Decompile klicken:

String myString = "Logged in as " + user.getDisplayName();

Wenn wir in Java auf diese zugreifen möchten, müssen wir den Namen des Getters explizit angeben.

In den meisten Fällen ist der Java-Name von Gettern für Kotlin-Properties einfach get + der Property-Name, wie wir bei User.getHasSystemAccess() und User.getDisplayName() gesehen haben. Die einzige Ausnahme sind Attribute, deren Namen mit „is“ beginnen. In diesem Fall ist der Java-Name für den Getter der Name der Kotlin-Property.

Beispiel: Eine Property für User:

val isAdmin get() = //...

In Java würde der Zugriff so erfolgen:

boolean userIsAnAdmin = user.isAdmin();

Wenn Sie die Annotation @JvmName verwenden, generiert Kotlin Bytecode mit dem angegebenen Namen anstelle des Standardnamens für das annotierte Element.

Das funktioniert genauso für Setter, deren generierte Namen immer set + Eigenschaftsname sind. Nehmen wir beispielsweise die folgende Klasse:

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

Angenommen, wir möchten den Setter-Namen von setRed() in updateRed() ändern, die Getter aber beibehalten. Dazu können wir die @set:JvmName-Version verwenden:

class Color {
   @set:JvmName("updateRed")
   var red = 0f
   @set:JvmName("updateGreen")
   var green = 0f
   @set:JvmName("updateBlue")
   var blue = 0f
}

In Java können wir dann Folgendes schreiben:

color.updateRed(0.8f);

UseCase.formatUser() verwendet den direkten Feldzugriff, um die Werte der Eigenschaften eines User-Objekts abzurufen.

In Kotlin werden Attribute normalerweise über Getter- und Setter-Methoden verfügbar gemacht. Dazu gehören val-Eigenschaften.

Sie können dieses Verhalten mit der @JvmField-Annotation ändern. Wenn dies auf ein Attribut in einer Klasse angewendet wird, werden in Kotlin keine Getter-Methoden (und keine Setter-Methoden für var-Attribute) generiert und auf das Sicherungsfeld kann direkt zugegriffen werden.

Da User-Objekte unveränderlich sind, möchten wir jede ihrer Eigenschaften als Felder verfügbar machen. Daher annotieren wir jede von ihnen mit @JvmField:

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   @get:JvmName("hasSystemAccess")
   val hasSystemAccess
       get() = "sys" in groups
}

Wenn wir uns UseCase.formatUser() noch einmal ansehen, sehen wir, dass die Fehler behoben wurden.

@JvmField oder const

Außerdem gibt es einen weiteren ähnlich aussehenden Fehler in der UseCase.java-Datei:

Repository.saveAs(Repository.BACKUP_PATH);

Wenn wir hier die automatische Vervollständigung verwenden, sehen wir, dass es ein Repository.getBACKUP_PATH() gibt. Es könnte also verlockend sein, die Anmerkung für BACKUP_PATH von @JvmStatic zu @JvmField zu ändern.

Probieren wir es aus. Wechseln Sie zurück zu Repository.kt und aktualisieren Sie die Annotation:

object Repository {
   @JvmField
   val BACKUP_PATH = "/backup/user.repo"

Wenn wir uns jetzt UseCase.java ansehen, ist der Fehler verschwunden. Es gibt aber auch einen Hinweis zu BACKUP_PATH:

In Kotlin können nur primitive Typen wie int, float und String const sein. Da BACKUP_PATH in diesem Fall ein String ist, können wir eine bessere Leistung erzielen, wenn wir const val anstelle eines mit @JvmField annotierten val verwenden. Gleichzeitig bleibt die Möglichkeit erhalten, auf den Wert als Feld zuzugreifen.

Das ändern wir jetzt in „Repository.kt“:

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

Wenn wir uns UseCase.java noch einmal ansehen, sehen wir, dass nur noch ein Fehler vorhanden ist.

Der letzte Fehler lautet Exception: 'java.io.IOException' is never thrown in the corresponding try block.

Wenn wir uns den Code für Repository.saveAs in Repository.kt ansehen, stellen wir fest, dass er eine Ausnahme auslöst. Was ist da los?

In Java gibt es das Konzept einer „geprüften Ausnahme“. Das sind Ausnahmen, die behoben werden können, z. B. wenn der Nutzer einen Dateinamen falsch eingegeben hat oder das Netzwerk vorübergehend nicht verfügbar ist. Nachdem eine geprüfte Ausnahme abgefangen wurde, kann der Entwickler dem Nutzer Feedback dazu geben, wie das Problem behoben werden kann.

Da geprüfte Ausnahmen zur Kompilierzeit geprüft werden, deklarieren Sie sie in der Signatur der Methode:

public void openFile(File file) throws FileNotFoundException {
   // ...
}

Kotlin hat jedoch keine geprüften Ausnahmen, was hier das Problem verursacht.

Die Lösung besteht darin, Kotlin aufzufordern, die möglicherweise ausgelöste IOException der Signatur von Repository.saveAs() hinzuzufügen, damit sie im JVM-Bytecode als geprüfte Ausnahme enthalten ist.

Dazu verwenden wir die Kotlin-Annotation @Throws, die die Java-/Kotlin-Interoperabilität unterstützt. In Kotlin verhalten sich Ausnahmen ähnlich wie in Java. Im Gegensatz zu Java gibt es in Kotlin jedoch nur ungeprüfte Ausnahmen. Wenn Sie also Ihren Java-Code darüber informieren möchten, dass eine Kotlin-Funktion eine Ausnahme auslöst, müssen Sie die Annotation @Throws in der Kotlin-Funktionssignatur verwenden. Wechseln Sie zu Repository.kt file und aktualisieren Sie saveAs(), um die neue Annotation einzufügen:

@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
   val outputFile = File(path)
   if (!outputFile.canWrite()) {
       throw FileNotFoundException("Could not write to file: $path")
   }
   // Write data...
}

Dank der Annotation @Throws sehen wir, dass alle Compilerfehler in UseCase.java behoben wurden. Super!

Vielleicht fragen Sie sich, ob Sie jetzt try- und catch-Blöcke verwenden müssen, wenn Sie saveAs() aus Kotlin aufrufen.

Nein. Denken Sie daran, dass es in Kotlin keine geprüften Ausnahmen gibt. Das Hinzufügen von @Throws zu einer Methode ändert daran nichts:

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

Es ist weiterhin sinnvoll, Ausnahmen abzufangen, wenn sie behandelt werden können, aber Kotlin zwingt Sie nicht dazu.

In diesem Codelab haben wir die Grundlagen für das Schreiben von Kotlin-Code behandelt, der auch das Schreiben von idiomatischem Java-Code unterstützt.

Wir haben darüber gesprochen, wie wir mit Annotationen die Art und Weise ändern können, wie Kotlin seinen JVM-Bytecode generiert, z. B.:

  • @JvmStatic zum Generieren statischer Elemente und Methoden.
  • @JvmOverloads, um überladene Methoden für Funktionen mit Standardwerten zu generieren.
  • @JvmName, um den Namen von Gettern und Settern zu ändern.
  • @JvmField, um eine Property direkt als Feld und nicht über Getter- und Setter-Methoden verfügbar zu machen.
  • @Throws, um geprüfte Ausnahmen zu deklarieren.

Die endgültigen Inhalte unserer Dateien sind:

User.kt

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   val hasSystemAccess
       @JvmName("hasSystemAccess")
       get() = "sys" in groups
}

Repository.kt

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   @Throws(IOException::class)
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

StringUtils.kt

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

fun String.toTitleCase(): String {
   if (isNullOrBlank()) {
       return this
   }

   return split(" ").map { word ->
       word.foldIndexed("") { index, working, char ->
           val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
           "$working$nextChar"
       }
   }.reduceIndexed { index, working, word ->
       if (index > 0) "$working $word" else word
   }
}

fun String.nameToLogin(): String {
   if (isNullOrBlank()) {
       return this
   }
   var working = ""
   toCharArray().forEach { char ->
       if (char.isLetterOrDigit()) {
           working += char.toLowerCase()
       } else if (char.isWhitespace() and !working.endsWith(".")) {
           working += "."
       }
   }
   return working
}