Dans cet atelier de programmation, vous allez apprendre à écrire ou à adapter votre code Kotlin afin de le rendre plus facile à appeler depuis le code Java.
Points abordés
- Utiliser
@JvmField
,@JvmStatic
et d'autres annotations - Limites de l'accès à certaines fonctionnalités du langage Kotlin à partir du code Java.
Ce que vous devez savoir
Cet atelier de programmation s'adresse aux programmeurs et suppose des connaissances de base sur Java et Kotlin.
Cet atelier de programmation simule la migration d'une partie d'un projet plus important écrit avec le langage de programmation Java, afin d'intégrer un nouveau code Kotlin.
Pour simplifier les choses, nous aurons un seul fichier .java
appelé UseCase.java
, qui représentera le codebase existant.
Notez que nous venons de remplacer une fonctionnalité initialement rédigée en Java par une nouvelle version écrite en Kotlin, et nous devons terminer son intégration.
Importer le projet
Le code du projet peut être cloné depuis le projet GitHub ici: GitHub
Vous pouvez également télécharger et extraire le projet à partir d'une archive ZIP disponible ici:
Si vous utilisez IntelliJ IDEA, sélectionnez "Importer le projet".
Si vous utilisez Android Studio, sélectionnez "Importer un projet" (Gradle, Eclipse ADT, etc.).
Ouvrez UseCase.java
et commençons à corriger les erreurs détectées.
La première fonction qui pose problème est registerGuest
:
public static User registerGuest(String name) {
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Repository.addUser(guest);
return guest;
}
Les erreurs pour Repository.getNextGuestId()
et Repository.addUser(...)
sont les mêmes: "Non statique est inaccessible depuis un contexte statique."
Maintenant, examinons l'un des fichiers Kotlin. Ouvrez le fichier Repository.kt
.
Nous constatons que notre Repository est un singleton déclaré à l'aide du mot clé "object". Le problème est que Kotlin génère une instance statique au sein de notre classe, plutôt que de les exposer en tant que propriétés et méthodes statiques.
Par exemple, Repository.getNextGuestId()
peut être référencé à l'aide de Repository.INSTANCE.getNextGuestId()
, mais il existe un meilleur moyen de l'utiliser.
Nous pouvons demander à Kotlin de générer des méthodes et des propriétés statiques en annotant les propriétés publiques et les méthodes du Repository avec @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)
}
}
Ajoutez l'annotation @JvmStatic à votre code à l'aide de votre IDE.
Si vous revenez à UseCase.java
, les propriétés et les méthodes sur Repository
ne génèrent plus d'erreurs, à l'exception de Repository.BACKUP_PATH
. Nous y reviendrons plus tard.
Pour l'instant, corrigeons l'erreur suivante dans la méthode registerGuest()
.
Considérons le scénario suivant: nous avions une classe StringUtils
avec plusieurs fonctions statiques pour les opérations de chaîne. Lorsque nous l'avons convertie en Kotlin, les méthodes sont converties en fonctions d'extension. Java n'a pas de fonctions d'extension. Kotlin compile donc ces méthodes en tant que fonctions statiques.
Malheureusement, en examinant la méthode registerGuest()
dans UseCase.java
, nous pouvons voir que quelque chose n'est pas correct:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Cela est dû au fait que Kotlin place ces fonctions de niveau supérieur ou de package dans une classe dont le nom est basé sur le nom de fichier. Dans ce cas, comme le fichier est nommé StringUtils.kt, la classe correspondante est nommée StringUtilsKt
.
Nous pourrions remplacer toutes nos références à StringUtils
par StringUtilsKt
et corriger cette erreur, mais cette solution n'est pas idéale pour les raisons suivantes:
- Notre code peut contenir de nombreux éléments qui doivent être mis à jour.
- Le nom lui-même est gênant.
Plutôt que de refactoriser notre code Java, ajoutons et mettons à jour le code Kotlin de ces méthodes.
Ouvrez StringUtils.Kt
, puis recherchez la déclaration de package suivante:
package com.google.example.javafriendlykotlin
Pour indiquer à Kotlin d'utiliser un autre nom pour les méthodes au niveau du package, utilisez l'annotation @file:JvmName
. Utilisons cette annotation pour nommer la classe StringUtils
.
@file:JvmName("StringUtils")
package com.google.example.javafriendlykotlin
Si vous revenez à UseCase.java
, vous constaterez que l'erreur pour StringUtils.nameToLogin()
a été corrigée.
Malheureusement, cette erreur a été remplacée par une nouvelle sur les paramètres transmis au constructeur de User
. Passons à l'étape suivante pour corriger cette dernière erreur dans UseCase.registerGuest()
.
Kotlin accepte les valeurs par défaut pour les paramètres. Pour voir comment elles sont utilisées, regardez dans le bloc init
de Repository.kt
.
Repository.kt:
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
Nous constatons que, pour l'utilisateur, nous pouvons ignorer la valeur de displayName
, car il s'agit d'une valeur par défaut spécifiée dans User.kt
.
User.kt:
data class User(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
Malheureusement, cette méthode ne fonctionne pas de la même manière lorsque vous appelez la méthode depuis Java.
UseCase.java:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Les valeurs par défaut ne sont pas acceptées dans le langage de programmation Java. Pour résoudre ce problème, demandons à Kotlin de générer des surcharges pour notre constructeur à l'aide de l'annotation @JvmOverloads.
Tout d'abord, nous devons apporter une légère modification à User.kt
.
Comme la classe User
ne comporte qu'un seul constructeur principal, et que le constructeur n'inclut aucune annotation, le mot clé constructor
a été omis. Maintenant que nous souhaitons l'annoter, le mot clé constructor
doit être inclus:
data class User constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
En ajoutant le mot clé constructor
, vous pouvez ajouter l'annotation @JvmOverloads
:
data class User @JvmOverloads constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
Si nous revenons au UseCase.java
, nous constatons qu'il n'y a plus d'erreurs dans la fonction registerGuest
.
L'étape suivante consiste à résoudre le problème d'appel de user.hasSystemAccess()
dans UseCase.getSystemUsers()
. Passez à l'étape suivante pour résoudre le problème ou poursuivez votre lecture pour découvrir plus en détail comment @JvmOverloads
a corrigé l'erreur.
@JvmOverload
Pour mieux comprendre ce que fait @JvmOverloads
, créez une méthode de test dans 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);
}
Nous pouvons construire un User
avec seulement deux paramètres, id
et username
:
User syrinx = new User(1001, "syrinx");
Nous pouvons également construire un User
en incluant un troisième paramètre pour displayName
tout en utilisant la valeur par défaut pour groups
:
User ione = new User(1002, "ione", "Ione Saldana");
En revanche, il n'est pas possible d'ignorer displayName
et d'indiquer simplement une valeur pour groups
sans écrire de code supplémentaire:
Nous allons donc supprimer cette ligne ou la remplacer par "//' pour la commenter.
En langage Kotlin, si nous souhaitons combiner des paramètres par défaut et des paramètres par défaut, nous devons utiliser des paramètres nommés.
// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))
En effet, Kotlin génère des surcharges pour les fonctions (y compris les constructeurs), mais ne crée qu'une surcharge par paramètre avec une valeur par défaut.
Revenons à UseCase.java
et tentons de résoudre le problème suivant: l'appel de user.hasSystemAccess()
dans la méthode 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;
}
C'est une erreur intéressante ! Si vous utilisez la fonctionnalité de saisie semi-automatique de votre IDE sur la classe User
, vous remarquerez que hasSystemAccess()
a été renommé getHasSystemAccess()
.
Pour résoudre le problème, nous souhaitons que Kotlin génère un autre nom pour la propriété val
hasSystemAccess
. Pour ce faire, nous pouvons utiliser l'annotation @JvmName
. Revenons à User.kt
et voyons où l'appliquer.
Il existe deux façons d'appliquer l'annotation. La première consiste à l'appliquer directement à la méthode get()
, comme suit:
val hasSystemAccess
@JvmName("hasSystemAccess")
get() = "sys" in groups
Ce code indique à Kotlin de remplacer la signature du getter explicitement défini par le nom fourni.
Vous pouvez également l'appliquer à la propriété à l'aide d'un préfixe get:
comme suit:
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
La méthode alternative est particulièrement utile pour les propriétés qui utilisent un getter par défaut défini par défaut. Exemple :
@get:JvmName("isActive")
val active: Boolean
Cela permet de modifier le nom du "getter" sans avoir à définir explicitement un getter.
Malgré cette distinction, vous pouvez utiliser celle qui vous convient le mieux. Dans les deux cas, Kotlin crée un getter nommé hasSystemAccess()
.
Si vous revenez à UseCase.java
, nous pouvons vérifier que getSystemUsers()
ne comporte plus d'erreurs.
L'erreur suivante vient de formatUser()
, mais si vous souhaitez en savoir plus sur la convention d'attribution de noms"getter"Kotlin, lisez la suite avant de passer à l'étape suivante.
Attribution de noms à Getter et Setter
Lorsque nous écrivons Kotlin, il est facile d'oublier de rédiger ce code, par exemple:
val myString = "Logged in as ${user.displayName}")
Il appelle en fait une fonction pour obtenir la valeur de displayName
. Pour le vérifier, accédez à Tools > Kotlin > Show Kotlin Bytecode (Afficher les codes Kotlin et afficher le code Kotlin) dans le menu, puis cliquez sur le bouton Decompile:
String myString = "Logged in as " + user.getDisplayName();
Lorsque nous souhaitons y accéder depuis Java, nous devons écrire explicitement le nom du getter.
Dans la plupart des cas, le nom Java des getters pour les propriétés Kotlin est simplement get
+ le nom de la propriété, comme nous l'avons vu avec User.getHasSystemAccess()
et User.getDisplayName()
. La seule exception concerne les établissements dont le nom commence par "est". Dans ce cas, le nom Java de la méthode "getter" est le nom de la propriété Kotlin.
Par exemple, une propriété sur User
telle que:
val isAdmin get() = //...
Accès à partir de Java avec:
boolean userIsAnAdmin = user.isAdmin();
En utilisant l'annotation @JvmName
, Kotlin génère le code d'octet correspondant au nom spécifié au lieu du code par défaut pour l'élément annoté.
Cela fonctionne de la même manière pour les setters, dont les noms générés sont toujours set
+ nom de propriété. Prenons la classe suivante:
class Color {
var red = 0f
var green = 0f
var blue = 0f
}
Imaginons que nous souhaitions remplacer le nom setter de setRed()
par updateRed()
, sans modifier les getters. Nous pouvons utiliser la version @set:JvmName
pour cela:
class Color {
@set:JvmName("updateRed")
var red = 0f
@set:JvmName("updateGreen")
var green = 0f
@set:JvmName("updateBlue")
var blue = 0f
}
En Java, nous pouvons écrire:
color.updateRed(0.8f);
UseCase.formatUser()
utilise l'accès direct aux champs pour obtenir les valeurs des propriétés d'un objet User
.
En langage Kotlin, les propriétés sont généralement exposées via des getters et des setters. Cela inclut les propriétés val
.
Vous pouvez modifier ce comportement en utilisant l'annotation @JvmField
. Lorsque ce paramètre est appliqué à une propriété dans une classe, Kotlin ignore les méthodes "getter" (et "setter" pour les propriétés var
) et le champ de support est accessible directement.
Étant donné que les objets User
sont immuables, nous souhaitons afficher chacune de leurs propriétés sous forme de champs. Nous allons donc annoter chacun d'entre eux avec @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
}
En examinant UseCase.formatUser()
, nous constatons maintenant que les erreurs ont été corrigées.
@JvmField ou const
Là encore, le fichier UseCase.java
comporte une autre erreur d'apparence similaire:
Repository.saveAs(Repository.BACKUP_PATH);
Si nous utilisons la saisie semi-automatique ici, nous constatons qu'il existe une Repository.getBACKUP_PATH()
. Il peut donc être tentant de remplacer l'annotation BACKUP_PATH
par @JvmStatic
par @JvmField
.
Faisons un essai ! Revenez à Repository.kt
et mettez à jour l'annotation:
object Repository {
@JvmField
val BACKUP_PATH = "/backup/user.repo"
Si nous examinons maintenant UseCase.java
, l'erreur disparaît, mais il y a aussi une remarque sur BACKUP_PATH
:
En langage Kotlin, les seuls types qui peuvent être définis comme const
sont les rôles primitifs, comme int
, float
et String
. Dans ce cas, comme BACKUP_PATH
est une chaîne, nous pouvons obtenir de meilleures performances en utilisant const val
au lieu d'un val
annoté avec @JvmField
, tout en conservant la possibilité d'accéder à la valeur en tant que champ.
Modifions maintenant cette URL dans Repository.kt:
object Repository {
const val BACKUP_PATH = "/backup/user.repo"
En examinant UseCase.java
, nous pouvons voir qu'il ne reste qu'une seule erreur.
La dernière erreur indique Exception: 'java.io.IOException' is never thrown in the corresponding try block.
Si nous examinons le code de Repository.saveAs
dans Repository.kt
, nous constatons qu'il génère une exception. Que se passe-t-il ?
Java a le concept d'exception exception vérifiée. Il s'agit d'exceptions qui peuvent être récupérées, par exemple lorsque l'utilisateur saisit un nom de fichier de manière trompeuse ou que le réseau est temporairement indisponible. Une fois qu'une exception vérifiée a été détectée, le développeur peut transmettre des commentaires à l'utilisateur pour lui indiquer comment résoudre le problème.
Comme les exceptions vérifiées sont vérifiées au moment de la compilation, vous les déclarez dans la signature de la méthode:
public void openFile(File file) throws FileNotFoundException {
// ...
}
En revanche, Kotlin ne fait pas l'objet d'exceptions vérifiées, et c'est ce qui pose problème.
La solution consiste à demander à Kotlin d'ajouter le IOException
qui peut être envoyé à la signature de Repository.saveAs()
, de sorte que le bytecode JVM l'inclue en tant qu'exception vérifiée.
Pour ce faire, nous utilisons l'annotation @Throws
du langage Kotlin, qui facilite l'interopérabilité Java/Kotlin. En langage Kotlin, les exceptions se comportent de la même manière que Java, mais contrairement à Java, Kotlin ne contient que les exceptions non vérifiées. Par conséquent, si vous souhaitez informer votre code Java qu'une fonction Kotlin génère une exception, vous devez utiliser l'annotation @Throws sur la signature de la fonction Kotlin passer à Repository.kt file
et mettre à jour saveAs()
pour inclure la nouvelle annotation:
@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...
}
Une fois l'annotation @Throws
en place, nous pouvons voir que toutes les erreurs de compilation dans UseCase.java
sont corrigées. Bravo,
Vous vous demandez peut-être si vous devez utiliser les blocs try
et catch
pour appeler saveAs()
depuis Kotlin maintenant.
Non. N'oubliez pas que Kotlin n'a pas vérifié les exceptions. L'ajout de @Throws
à une méthode ne modifie pas cela:
fun saveFromKotlin(path: String) {
Repository.saveAs(path)
}
S'il est utile d'identifier les exceptions lorsqu'elles peuvent être gérées, Kotlin ne vous force pas à les gérer.
Dans cet atelier de programmation, nous avons passé en revue les notions élémentaires permettant d'écrire du code Kotlin compatible avec l'écriture de code Java idiomatique.
Nous avons vu comment utiliser les annotations pour modifier la façon dont Kotlin génère son bytecode JVM, par exemple:
@JvmStatic
pour générer des membres et des méthodes statiques@JvmOverloads
pour générer des méthodes surchargées pour les fonctions ayant des valeurs par défaut.@JvmName
pour modifier le nom des getters et des setters.@JvmField
pour exposer une propriété directement en tant que champ, plutôt que via des getters et des setters.@Throws
pour déclarer les exceptions vérifiées.
Voici les contenus finaux de nos fichiers:
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
}