Chiamata Kotlin Code da Java

In questo codelab, imparerai a scrivere o adattare il codice Kotlin per renderlo più facilmente richiamabile dal codice Java.

Cosa imparerai a fare:

  • Come utilizzare @JvmField, @JvmStatic e altre annotazioni.
  • Limitazioni relative all'accesso a determinate funzionalità del linguaggio Kotlin dal codice Java.

Cosa devi sapere

Questo codelab è stato scritto per i programmatori e presuppone la conoscenza di base di Java e Kotlin.

Questo codelab simula la migrazione di una parte di un progetto più ampio scritto con il linguaggio di programmazione Java, per incorporare un nuovo codice Kotlin.

Per semplificare le cose, avremo un unico file .java denominato UseCase.java, che rappresenterà il codebase esistente.

Immaginiamo di aver appena sostituito una funzionalità originariamente scritta in Java con una nuova versione scritta in Kotlin e che dobbiamo completare l'integrazione.

Importa il progetto

Il codice del progetto può essere clonato dal progetto GitHub qui: GitHub

In alternativa, puoi scaricare ed estrarre il progetto da un archivio ZIP disponibile qui:

Scarica Zip

Se utilizzi IntelliJ IDEA, seleziona "Importa progetto".

Se utilizzi Android Studio, seleziona "Importa progetto (Gradle, Eclipse ADT ecc.)".

Apriamo UseCase.java e inizia a risolvere gli errori che vediamo.

La prima funzione con un problema è registerGuest:

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

Gli errori sia per Repository.getNextGuestId() sia per Repository.addUser(...) sono gli stessi: "Non statico non è accessibile da un contesto statico."

Ora diamo un'occhiata a uno dei file Kotlin. Apri il file Repository.kt.

Abbiamo visto che il nostro Repository è un'unica dichiarazione dichiarata utilizzando la parola chiave dell'oggetto. Il problema è che Kotlin sta generando un'istanza statica all'interno della nostra classe, anziché esporla come proprietà e metodi statici.

Ad esempio, Repository.getNextGuestId() si può fare riferimento utilizzando Repository.INSTANCE.getNextGuestId(), ma c'è un modo migliore.

Possiamo richiedere a Kotlin di generare metodi e proprietà statici annotando le proprietà e i metodi pubblici del repository con @JvmStatic:

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

Aggiungi l'annotazione @JvmStatic al codice utilizzando il tuo IDE.

Se torniamo a UseCase.java, le proprietà e i metodi su Repository non causano più errori, tranne che per Repository.BACKUP_PATH. Torneremo su questo punto in seguito.

Per adesso, correggiamo l'errore successivo nel metodo registerGuest().

Consideriamo il seguente scenario: abbiamo una classe StringUtils con diverse funzioni statiche per le operazioni stringa. Quando lo abbiamo convertito in Kotlin, abbiamo convertito i metodi in funzioni di estensione. Java non dispone di funzioni di estensione, pertanto Kotlin compila questi metodi come funzioni statiche.

Purtroppo, esaminando il metodo registerGuest() all'interno di UseCase.java, possiamo vedere che qualcosa non va bene:

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

Il motivo è che Kotlin posiziona queste funzioni "a livello superiore" o a livello di pacchetto all'interno di una classe il cui nome è basato sul nome file. In questo caso, poiché il file è denominato StringUtils.kt, la classe corrispondente è denominata StringUtilsKt.

Abbiamo potuto modificare tutti i nostri riferimenti di StringUtils a StringUtilsKt e correggere questo errore, ma non è ideale perché:

  • È possibile che ci siano molti punti da aggiornare nel nostro codice.
  • Il nome in sé è imbarazzante.

Pertanto, anziché ridefinire il codice Java, aggiorniamo il codice Kotlin per utilizzare un nome diverso per questi metodi.

Apri StringUtils.Kt e cerca la seguente dichiarazione relativa al pacchetto:

package com.google.example.javafriendlykotlin

Possiamo indicare a Kotlin di utilizzare un nome diverso per i metodi a livello di pacchetto utilizzando l'annotazione @file:JvmName. Usiamo questa annotazione per assegnare un nome alla classe StringUtils.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Ora, esaminando UseCase.java, possiamo vedere che l'errore per StringUtils.nameToLogin() è stato risolto.

Purtroppo, questo errore è stato sostituito da uno nuovo sui parametri trasmessi al costruttore per User. Procedi al passaggio successivo e correggi quest'ultimo errore in UseCase.registerGuest().

Kotlin supporta i valori predefiniti per i parametri. Possiamo vedere come vengono utilizzati guardando all'interno del blocco init di Repository.kt.

Repository.kt:

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

Possiamo notare che per l'utente "warlow" possiamo saltare la valorizzazione di displayName perché c'è un valore predefinito specificato in User.kt.

User.kt:

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

Purtroppo non funziona quando chiami il metodo da Java.

UseCase.java:

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

I valori predefiniti non sono supportati nel linguaggio di programmazione Java. Per correggere questo, di' a Kotlin di generare sovraccarichi per il nostro costruttore con l'aiuto dell'annotazione @JvmOverloads.

Dobbiamo apportare una leggera modifica a User.kt.

Poiché la classe User ha un solo costruttore principale e il costruttore non include annotazioni, la parola chiave constructor è stata omessa. Adesso, per aggiungere una nota, è necessario includere la parola chiave constructor:

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

Se è presente la parola chiave constructor, possiamo aggiungere l'annotazione @JvmOverloads:

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

Se torniamo a UseCase.java, vediamo che non ci sono altri errori nella funzione registerGuest.

Il prossimo passaggio è correggere la chiamata interrotta al numero user.hasSystemAccess() in UseCase.getSystemUsers(). Vai al passaggio successivo oppure continua a leggere per approfondire ciò che @JvmOverloads ha fatto per correggere l'errore.

@JvmOverload

Per capire meglio cosa fa @JvmOverloads, creiamo un metodo di test 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);
}

Possiamo creare un elemento User con due soli parametri, id e username:

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

Possiamo anche creare un User includendo un terzo parametro per displayName continuando a utilizzare il valore predefinito di groups:

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

Tuttavia, non è possibile ignorare displayName e fornire un valore per groups senza scrivere altro codice:

Elimina la riga o anteponi "//&#39".

In Kotlin, se vogliamo combinare i parametri predefiniti e non predefiniti, dobbiamo utilizzare i parametri denominati.

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

Il motivo è che Kotlin genererà sovraccarichi per le funzioni, inclusi i costruttori, ma creerà un sovraccarico per parametro con un valore predefinito.

Rivediamo UseCase.java e risolviamo il nostro prossimo problema: la chiamata a user.hasSystemAccess() nel metodo 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;
}

Questo è un errore interessante. Se utilizzi la funzionalità di completamento automatico di IDE per il corso User, noterai che il nome hasSystemAccess() è stato rinominato in getHasSystemAccess().

Per risolvere il problema, vorremmo che Kotlin generasse un nome diverso per la proprietà val hasSystemAccess. Per farlo, possiamo utilizzare l'annotazione @JvmName. Torniamo a User.kt e vediamo dove dovremmo applicarla.

Esistono due modi per applicare l'annotazione. Il primo è applicarlo direttamente al metodo get(), in questo modo:

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

In questo modo, Kotlin modifica la firma del getter definito esplicitamente nel nome fornito.

In alternativa, è possibile applicarlo alla proprietà utilizzando un prefisso get: come segue:

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

Il metodo alternativo è particolarmente utile per le proprietà che utilizzano un getter predefinito implicitamente definito. Ad esempio:

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

Ciò consente di modificare il nome del getter senza dover definire esplicitamente un getter.

Nonostante questa distinzione, puoi utilizzare come preferisci. Entrambi causeranno la creazione di un getter con il nome hasSystemAccess().

Se torniamo a UseCase.java, possiamo verificare che ora getSystemUsers() non presenti errori.

Il prossimo errore è in formatUser(), ma se vuoi leggere di più sulla convenzione di denominazione dei getter Kotlin, continua a leggere qui prima di andare al passaggio successivo.

Nome e getter

Quando scrivi Kotlin, è facile dimenticare questo codice, ad esempio:

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

Sta per chiamare una funzione per ottenere il valore di displayName. Per verificarlo, accedi a Strumenti > Kotlin > Mostra Kotlin Bytecode nel menu, quindi fai clic sul pulsante Decompile:

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

Quando vogliamo accedervi da Java, dobbiamo scrivere esplicitamente il nome del getter.

Nella maggior parte dei casi, il nome Java del getter per le proprietà Kotlin è semplicemente get + il nome della proprietà, come abbiamo visto con User.getHasSystemAccess() e User.getDisplayName(). L'unica eccezione è rappresentata dalle proprietà i cui nomi iniziano con "quot;is". In questo caso, il nome Java per il getter è il nome della proprietà Kotlin.

Ad esempio, una proprietà su User come:

val isAdmin get() = //...

Sarebbe accessibile da Java con:

boolean userIsAnAdmin = user.isAdmin();

Utilizzando l'annotazione @JvmName, Kotlin genera bytecode che contiene il nome specificato, anziché quello predefinito, per l'elemento annotato.

Lo stesso vale per i setter i cui nomi generati sono sempre set + nome della proprietà. Ad esempio, segui il seguente corso:

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

Immaginiamo di voler cambiare il nome del setter da setRed() a updateRed(), lasciando da solo i getter. Possiamo usare la versione @set:JvmName per fare questo:

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

Da Java, possiamo scrivere:

color.updateRed(0.8f);

UseCase.formatUser() utilizza l'accesso diretto al campo per ottenere i valori delle proprietà di un oggetto User.

In Kotlin, le proprietà sono normalmente esposte tramite getter e setter. Sono incluse le proprietà val.

È possibile cambiare questo comportamento utilizzando l'annotazione @JvmField. Quando quest'ultimo viene applicato a una proprietà di un corso, Kotlin salta la generazione dei metodi getter (e setter per le proprietà var) e il campo di supporto è accessibile direttamente.

Poiché gli oggetti User sono immutabili, vorremmo esporre ciascuna delle loro proprietà come campi, quindi aggiungeremo una nota a ciascuna di esse con @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
}

Se ora esaminiamo UseCase.formatUser(), possiamo vedere che gli errori sono stati corretti.

@JvmField o const

C'è un altro errore simile nel file UseCase.java:

Repository.saveAs(Repository.BACKUP_PATH);

Se utilizziamo il completamento automatico qui, possiamo vedere che c'è un Repository.getBACKUP_PATH(), quindi potrebbe essere tentata di cambiare l'annotazione su BACKUP_PATH da @JvmStatic a @JvmField.

Prova. Torna a Repository.kt e aggiorna l'annotazione:

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

Se diamo un'occhiata a UseCase.java adesso, vedremo che l'errore si è risolto, ma c'è anche una nota su BACKUP_PATH:

In Kotlin, gli unici tipi che possono essere const sono primitive, come int, float e String. In questo caso, poiché BACKUP_PATH è una stringa, possiamo migliorare le prestazioni utilizzando const val invece di un val annotato con @JvmField, mantenendo la possibilità di accedere al valore come campo.

Cambiamo l'impostazione in Repository.kt:

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

Se diamo un'occhiata a UseCase.java, possiamo vedere un solo errore.

L'errore finale dice Exception: 'java.io.IOException' is never thrown in the corresponding try block.

Se osserviamo il codice di Repository.saveAs in Repository.kt, tuttavia, abbiamo notato un'eccezione. Qual è il problema?

Java ha il concetto di "eccezione selezionata". Si tratta di eccezioni che potrebbero essere recuperate, ad esempio l'utente che digita erroneamente un nome di file o che la rete non è temporaneamente disponibile. Una volta individuata un'eccezione selezionata, lo sviluppatore può fornire all'utente un feedback su come risolvere il problema.

Poiché le eccezioni verificate vengono verificate al momento della compilazione, vengono dichiarate nella firma del metodo:

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

Kotlin, invece, non ha verificato eccezioni e questa è la causa del problema.

La soluzione è chiedere a Kotlin di aggiungere IOException alla potenziale firma di Repository.saveAs(), in modo che il bytecode JVM lo includa come eccezione verificata.

A tal fine, utilizziamo l'annotazione Kotlin @Throws, che consente l'interoperabilità con Java/Kotlin. In Kotlin, le eccezioni si comportano in modo simile a Java, ma a differenza di Java, Kotlin ha solo eccezioni deselezionate. Pertanto, se vuoi comunicare al tuo codice Java che una funzione Kotlin genera un'eccezione, devi utilizzare l'annotazione @Throws per la firma della funzione Kotlin Passa a Repository.kt file e aggiorna saveAs() per includere la nuova annotazione:

@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...
}

Con l'annotazione @Throws attiva, possiamo vedere che tutti gli errori del compilatore in UseCase.java sono stati corretti. Complimenti!

Potresti chiederti se dovrai utilizzare i blocchi try e catch quando chiami saveAs() adesso da Kotlin.

No, Ricorda che Kotlin non ha controllato le eccezioni e che l'aggiunta di @Throws a un metodo non modificherà questo comportamento:

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

È ancora utile rilevare le eccezioni quando possono essere gestite, ma Kotlin non ti costringe a gestirle.

In questo codelab abbiamo esaminato le nozioni di base su come scrivere codice Kotlin che supporta anche la scrittura di codice Java idiomatico.

Abbiamo parlato di come utilizzare le annotazioni per cambiare il modo in cui Kotlin genera il suo bytecode JVM, ad esempio:

  • @JvmStatic per generare membri e metodi statici.
  • @JvmOverloads per generare metodi di sovraccarico per le funzioni con valori predefiniti.
  • @JvmName per modificare il nome di getter e setter.
  • @JvmField per esporre una proprietà direttamente come campo, anziché tramite getter e setter.
  • @Throws per dichiarare le eccezioni verificate.

I contenuti finali dei nostri file sono:

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

Stringa Utils.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
}