Chiamare codice Kotlin 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 all'accesso a determinate funzionalità del linguaggio Kotlin dal codice Java.

Cosa devi già sapere

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

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

Per semplificare le cose, avremo un unico file .java chiamato UseCase.java, che rappresenterà la base di codice esistente.

Supponiamo di aver appena sostituito alcune funzionalità originariamente scritte in Java con una nuova versione scritta in Kotlin e di dover completare l'integrazione.

Importare 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 e così via)".

Apriamo UseCase.java e iniziamo a risolvere gli errori visualizzati.

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 per Repository.getNextGuestId() e Repository.addUser(...) sono gli stessi: "Non-static cannot be accessed from a static context" (Non è possibile accedere a non statico da un contesto statico).

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

Vediamo che il nostro repository è un singleton dichiarato utilizzando la parola chiave object. Il problema è che Kotlin genera un'istanza statica all'interno della nostra classe, anziché esporle come proprietà e metodi statici.

Ad esempio, è possibile fare riferimento a Repository.getNextGuestId() utilizzando Repository.INSTANCE.getNextGuestId(), ma esiste un modo migliore.

Possiamo fare in modo che Kotlin generi 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 l'IDE.

Se torniamo a UseCase.java, le proprietà e i metodi di Repository non causano più errori, ad eccezione di Repository.BACKUP_PATH. Ci torneremo più avanti.

Per il momento, correggiamo il prossimo errore nel metodo registerGuest().

Consideriamo lo scenario seguente: avevamo una classe StringUtils con diverse funzioni statiche per le operazioni sulle stringhe. Quando l'abbiamo convertita in Kotlin, abbiamo convertito i metodi in funzioni di estensione. Java non dispone di funzioni di estensione, quindi Kotlin compila questi metodi come funzioni statiche.

Purtroppo, se esaminiamo il metodo registerGuest() all'interno di UseCase.java, possiamo notare che qualcosa non va:

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

Il motivo è che Kotlin inserisce queste funzioni "di primo livello" o a livello di pacchetto all'interno di una classe il cui nome si basa sul nome del file. In questo caso, poiché il file si chiama StringUtils.kt, la classe corrispondente si chiama StringUtilsKt.

Potremmo modificare tutti i nostri riferimenti a StringUtils in StringUtilsKt e correggere questo errore, ma non è l'ideale perché:

  • Potrebbero esserci molti punti nel nostro codice che devono essere aggiornati.
  • Il nome stesso è strano.

Quindi, anziché eseguire il refactoring del codice Java, aggiorniamo il codice Kotlin in modo da utilizzare un nome diverso per questi metodi.

Apri StringUtils.Kt e trova la seguente dichiarazione del 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. Utilizziamo questa annotazione per assegnare il nome StringUtils alla classe.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Se torniamo a UseCase.java, possiamo vedere che l'errore per StringUtils.nameToLogin() è stato risolto.

Purtroppo, questo errore è stato sostituito da uno nuovo relativo ai parametri passati al costruttore per User. Continuiamo con il passaggio successivo e correggiamo quest'ultimo errore in UseCase.registerGuest().

Kotlin supporta i valori predefiniti per i parametri. Possiamo vedere come vengono utilizzati esaminando il 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 vedere che per l'utente "warlow" possiamo saltare l'inserimento di un valore per displayName perché è stato specificato un valore predefinito 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, questo non funziona allo stesso modo quando si chiama 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 risolvere il problema, diciamo a Kotlin di generare overload per il nostro costruttore con l'aiuto dell'annotazione @JvmOverloads.

Innanzitutto, dobbiamo apportare un piccolo aggiornamento a User.kt.

Poiché la classe User ha un solo costruttore primario e il costruttore non include annotazioni, la parola chiave constructor è stata omessa. Ora che vogliamo annotarlo, tuttavia, deve essere inclusa 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")
)

Con la parola chiave constructor presente, 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, possiamo vedere che non ci sono più errori nella funzione registerGuest.

Il passaggio successivo consiste nel correggere la chiamata interrotta a user.hasSystemAccess() in UseCase.getSystemUsers(). Continua con il passaggio successivo oppure continua a leggere per scoprire di più su cosa ha fatto @JvmOverloads per correggere l'errore.

@JvmOverloads

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 costruire un User con solo due 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 per groups:

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

Tuttavia, non è possibile saltare displayName e fornire solo un valore per groups senza scrivere codice aggiuntivo:

Quindi, eliminiamo la riga o aggiungiamo il prefisso "//" per commentarla.

In Kotlin, se vogliamo combinare parametri predefiniti e non predefiniti, dobbiamo utilizzare 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à overload per le funzioni, inclusi i costruttori, ma creerà un solo overload per parametro con un valore predefinito.

Torniamo a UseCase.java e affrontiamo 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 dell'IDE nella classe User, noterai che 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 dobbiamo applicarlo.

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

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

Questo indica a Kotlin di modificare la firma del getter definito in modo esplicito con il nome fornito.

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

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

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

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

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

Nonostante questa distinzione, puoi utilizzare quello che preferisci. Entrambi faranno in modo che Kotlin crei un getter con il nome hasSystemAccess().

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

L'errore successivo si trova in formatUser(), ma se vuoi saperne di più sulla convenzione di denominazione dei getter Kotlin, continua a leggere qui prima di passare al passaggio successivo.

Denominazione di getter e setter

Quando scriviamo in Kotlin, è facile dimenticare che scrivere codice come:

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

Chiama effettivamente una funzione per ottenere il valore di displayName. Possiamo verificarlo andando su Strumenti > Kotlin > Mostra bytecode Kotlin nel menu e poi facendo clic sul pulsante Decompila:

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 dei getter per le proprietà Kotlin è semplicemente get + il nome della proprietà, come abbiamo visto con User.getHasSystemAccess() e User.getDisplayName(). L'unica eccezione sono le proprietà i cui nomi iniziano con "is". In questo caso, il nome Java del getter è il nome della proprietà Kotlin.

Ad esempio, una proprietà su User come:

val isAdmin get() = //...

Si accede da Java con:

boolean userIsAnAdmin = user.isAdmin();

Utilizzando l'annotazione @JvmName, Kotlin genera bytecode con 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, prendi in considerazione il seguente corso:

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

Supponiamo di voler modificare il nome del setter da setRed() a updateRed(), lasciando invariati i getter. Potremmo utilizzare la versione @set:JvmName per farlo:

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

Da Java, potremmo quindi scrivere:

color.updateRed(0.8f);

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

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

È possibile modificare questo comportamento utilizzando l'annotazione @JvmField. Quando viene applicato a una proprietà in una classe, 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, vogliamo esporre ciascuna delle loro proprietà come campi, quindi annoteremo ciascuna di queste 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 torniamo a UseCase.formatUser(), ora possiamo vedere che gli errori sono stati corretti.

@JvmField o const

Inoltre, nel file UseCase.java è presente un altro errore simile:

Repository.saveAs(Repository.BACKUP_PATH);

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

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

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

Se ora esaminiamo UseCase.java, vedremo che l'errore è scomparso, ma c'è anche una nota su BACKUP_PATH:

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

Modifichiamo ora questo aspetto in Repository.kt:

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

Se esaminiamo UseCase.java, possiamo vedere che è rimasto un solo errore.

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

Se esaminiamo il codice per Repository.saveAs in Repository.kt, vediamo che genera un'eccezione. Che cosa succede?

Java ha il concetto di "eccezione controllata". Si tratta di eccezioni che possono essere recuperate, ad esempio l'utente che digita in modo errato un nome file o la rete che non è temporaneamente disponibile. Dopo che un'eccezione controllata viene rilevata, lo sviluppatore può fornire all'utente un feedback su come risolvere il problema.

Poiché le eccezioni controllate vengono verificate in fase di compilazione, le dichiari nella firma del metodo:

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

Kotlin, invece, non ha eccezioni controllate ed è questo che sta causando il problema.

La soluzione consiste nel chiedere a Kotlin di aggiungere IOException potenzialmente generato alla firma di Repository.saveAs(), in modo che il bytecode JVM lo includa come eccezione controllata.

Lo facciamo con l'annotazione @Throws di Kotlin, che aiuta l'interoperabilità Java/Kotlin. In Kotlin, le eccezioni si comportano in modo simile a Java, ma a differenza di Java, Kotlin ha solo eccezioni non controllate. Pertanto, se vuoi comunicare al tuo codice Java che una funzione Kotlin genera un'eccezione, devi utilizzare l'annotazione @Throws nella 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, possiamo vedere che tutti gli errori del compilatore in UseCase.java sono stati corretti. Evviva!

Ti starai chiedendo se ora dovrai utilizzare i blocchi try e catch quando chiami saveAs() da Kotlin.

No. Ricorda che Kotlin non ha eccezioni controllate e l'aggiunta di @Throws a un metodo non cambia la situazione:

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

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

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

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

  • @JvmStatic per generare membri e metodi statici.
  • @JvmOverloads per generare metodi di overload per le funzioni che hanno 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 selezionate.

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

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
}