Wywoływanie kodu Kotlin z Javy

Z tego laboratorium dowiesz się, jak napisać lub dostosować kod w Kotlinie, aby można go było łatwiej wywoływać z kodu w Javie.

Czego się nauczysz

  • Jak korzystać z adnotacji @JvmField, @JvmStatic i innych.
  • Ograniczenia w dostępie do niektórych funkcji języka Kotlin z kodu Java.

Co musisz już wiedzieć

Te warsztaty są przeznaczone dla programistów i zakładają podstawową znajomość języków Java i Kotlin.

Te warsztaty pokazują, jak przenieść część większego projektu napisanego w języku programowania Java, aby uwzględnić nowy kod w języku Kotlin.

Aby uprościć sprawę, będziemy mieć jeden plik .java o nazwie UseCase.java, który będzie reprezentować istniejącą bazę kodu.

Załóżmy, że zastąpiliśmy część funkcji pierwotnie napisanych w Javie nową wersją napisaną w Kotlinie i musimy dokończyć jej integrację.

Importowanie projektu

Kod projektu można sklonować z projektu GitHub tutaj: GitHub

Możesz też pobrać i wyodrębnić projekt z archiwum ZIP, które znajdziesz tutaj:

Pobierz plik ZIP

Jeśli używasz IntelliJ IDEA, wybierz „Import Project” (Importuj projekt).

Jeśli używasz Android Studio, wybierz „Importuj projekt (Gradle, Eclipse ADT itp.)”.

Otwórzmy UseCase.java i zacznijmy rozwiązywać wyświetlane błędy.

Pierwsza funkcja, w której wystąpił problem, to registerGuest:

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

Błędy w przypadku obu tych funkcji (Repository.getNextGuestId()Repository.addUser(...)) są takie same: „Non-static cannot be accessed from a static context” (Nie można uzyskać dostępu do funkcji niestatycznej z kontekstu statycznego).

Przyjrzyjmy się teraz jednemu z plików Kotlin. Otwórz plik Repository.kt.

Widzimy, że nasze repozytorium jest singletonem zadeklarowanym za pomocą słowa kluczowego object. Problem polega na tym, że Kotlin generuje w naszej klasie statyczną instancję, zamiast udostępniać ją jako statyczne właściwości i metody.

Na przykład do Repository.getNextGuestId() można się odwołać za pomocą Repository.INSTANCE.getNextGuestId(), ale istnieje lepszy sposób.

Aby Kotlin generował metody i właściwości statyczne, musimy dodać adnotację @JvmStatic do publicznych właściwości i metod repozytorium:

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

Dodaj do kodu adnotację @JvmStatic za pomocą środowiska IDE.

Jeśli wrócimy do UseCase.java, właściwości i metody w Repository nie będą już powodować błędów, z wyjątkiem Repository.BACKUP_PATH. Wrócimy do tego później.

Na razie naprawmy kolejny błąd w metodzie registerGuest().

Rozważmy ten scenariusz: mamy klasę StringUtils z kilkoma statycznymi funkcjami do operacji na ciągach. Po przekonwertowaniu go na język Kotlin przekształciliśmy metody w funkcje rozszerzeń. Java nie ma funkcji rozszerzeń, więc Kotlin kompiluje te metody jako funkcje statyczne.

Niestety, jeśli przyjrzymy się metodzie registerGuest()UseCase.java, zauważymy, że coś jest nie tak:

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

Dzieje się tak, ponieważ Kotlin umieszcza te funkcje „najwyższego poziomu” lub na poziomie pakietu w klasie, której nazwa jest oparta na nazwie pliku. W tym przypadku, ponieważ plik nazywa się StringUtils.kt, odpowiednia klasa nazywa się StringUtilsKt.

Moglibyśmy zmienić wszystkie odwołania do StringUtils na StringUtilsKt i usunąć ten błąd, ale nie jest to idealne rozwiązanie, ponieważ:

  • W naszym kodzie może być wiele miejsc, które trzeba zaktualizować.
  • Sama nazwa jest niezręczna.

Zamiast refaktoryzować kod Java, zaktualizujmy kod Kotlin, aby używać innych nazw tych metod.

Otwórz StringUtils.Kt i znajdź tę deklarację pakietu:

package com.google.example.javafriendlykotlin

Możemy poinformować Kotlin, aby używał innej nazwy metod na poziomie pakietu, za pomocą @file:JvmNameadnotacji. Użyjmy tej adnotacji, aby nazwać klasę StringUtils.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Jeśli teraz wrócimy do UseCase.java, zobaczymy, że błąd dotyczący StringUtils.nameToLogin() został rozwiązany.

Niestety ten błąd został zastąpiony nowym, który dotyczy parametrów przekazywanych do konstruktora User. Przejdźmy do następnego kroku i naprawmy ten ostatni błąd w UseCase.registerGuest().

Kotlin obsługuje wartości domyślne parametrów. Możemy sprawdzić, jak są używane, zaglądając do bloku initRepository.kt.

Repository.kt:

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

Widzimy, że w przypadku użytkownika „warlow” możemy pominąć wpisywanie wartości w polu displayName, ponieważ w polu User.kt określono dla niego wartość domyślną.

User.kt:

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

Niestety nie działa to tak samo, gdy wywołujesz metodę z Javy.

UseCase.java:

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

Wartości domyślne nie są obsługiwane w języku programowania Java. Aby to naprawić, powiedzmy Kotlinowi, aby wygenerował przeciążenia dla naszego konstruktora za pomocą adnotacji @JvmOverloads.

Najpierw musimy wprowadzić niewielką zmianę w usłudze User.kt.

Klasa User ma tylko jeden konstruktor podstawowy, który nie zawiera żadnych adnotacji, więc słowo kluczowe constructor zostało pominięte. Teraz, gdy chcemy dodać do niego adnotację, musimy uwzględnić słowo kluczowe constructor:

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

Gdy występuje słowo kluczowe constructor, możemy dodać adnotację @JvmOverloads:

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

Jeśli wrócimy do UseCase.java, zobaczymy, że w funkcji registerGuest nie ma już żadnych błędów.

Następnym krokiem jest naprawienie nieprawidłowego wywołania funkcji user.hasSystemAccess() w pliku UseCase.getSystemUsers(). Aby to zrobić, przejdź do następnego kroku lub czytaj dalej, aby dowiedzieć się więcej o tym, co @JvmOverloads zrobił(-a), aby naprawić błąd.

@JvmOverloads

Aby lepiej zrozumieć, co robi @JvmOverloads, utwórzmy metodę testową w 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);
}

Możemy utworzyć User za pomocą tylko 2 parametrów: idusername:

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

Możemy też utworzyć User, dodając trzeci parametr do displayName, ale nadal używając wartości domyślnej dla groups:

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

Nie można jednak pominąć displayName i podać tylko wartości groups bez pisania dodatkowego kodu:

Usuń ten wiersz lub dodaj przed nim „//”, aby zmienić go w komentarz.

W języku Kotlin, jeśli chcemy połączyć parametry domyślne i inne, musimy użyć nazwanych parametrów.

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

Dzieje się tak, ponieważ Kotlin generuje przeciążenia funkcji, w tym konstruktorów, ale tworzy tylko jedno przeciążenie na parametr z wartością domyślną.

Wróćmy do kodu UseCase.java i rozwiążmy kolejny problem: wywołanie funkcji user.hasSystemAccess() w metodzie 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;
}

To ciekawy błąd. Jeśli użyjesz funkcji autouzupełniania w IDE w przypadku klasy User, zauważysz, że hasSystemAccess() została zmieniona na getHasSystemAccess().

Aby rozwiązać ten problem, chcemy, aby język Kotlin wygenerował inną nazwę właściwości valhasSystemAccess. Możemy do tego użyć adnotacji @JvmName. Wróćmy do User.kt i zobaczmy, gdzie powinniśmy go użyć.

Adnotację możemy zastosować na 2 sposoby. Pierwszy sposób polega na zastosowaniu go bezpośrednio w metodzie get(), jak w tym przykładzie:

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

Sygnalizuje to Kotlinowi, że ma zmienić sygnaturę jawnie zdefiniowanego gettera na podaną nazwę.

Możesz też zastosować go do właściwości, używając prefiksu get: w ten sposób:

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

Metoda alternatywna jest szczególnie przydatna w przypadku właściwości, które używają domyślnego, niejawnie zdefiniowanego gettera. Na przykład:

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

Dzięki temu można zmienić nazwę funkcji pobierającej bez konieczności jej jawnego definiowania.

Mimo tej różnicy możesz używać dowolnego z nich. W obu przypadkach Kotlin utworzy getter o nazwie hasSystemAccess().

Jeśli wrócimy do UseCase.java, możemy sprawdzić, czy getSystemUsers() nie zawiera już błędów.

Kolejny błąd znajduje się w formatUser(). Jeśli chcesz dowiedzieć się więcej o konwencji nazewnictwa getterów w języku Kotlin, przeczytaj ten artykuł przed przejściem do następnego kroku.

Nazewnictwo metod pobierających i ustawiających

Podczas pisania w Kotlinie łatwo zapomnieć, że kod taki jak:

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

wywołuje funkcję, aby uzyskać wartość displayName. Możemy to sprawdzić, wybierając w menu Narzędzia > Kotlin > Pokaż kod bajtowy Kotlina, a następnie klikając przycisk Decompile (Dekompiluj):

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

Jeśli chcemy uzyskać do nich dostęp z Javy, musimy jawnie wpisać nazwę metody pobierającej.

W większości przypadków nazwa getterów w Javie dla właściwości w Kotlinie to po prostu get + nazwa właściwości, jak w przypadku User.getHasSystemAccess() i User.getDisplayName(). Jedynym wyjątkiem są usługi, których nazwy zaczynają się od „is”. W tym przypadku nazwa gettera w Javie jest nazwą właściwości w Kotlinie.

Na przykład usługa w User, taka jak:

val isAdmin get() = //...

Dostęp do niego w Javie można uzyskać za pomocą tego kodu:

boolean userIsAnAdmin = user.isAdmin();

Dzięki adnotacji @JvmName Kotlin generuje kod bajtowy, który ma określoną nazwę, a nie domyślną, dla elementu, do którego dodano adnotację.

W przypadku funkcji ustawiających generowane nazwy zawsze mają postać set + nazwa właściwości. Weźmy na przykład tę klasę:

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

Załóżmy, że chcemy zmienić nazwę funkcji ustawiającej z setRed() na updateRed(), pozostawiając funkcje pobierające bez zmian. Możemy użyć wersji @set:JvmName:

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

W języku Java możemy wtedy napisać:

color.updateRed(0.8f);

UseCase.formatUser() korzysta z bezpośredniego dostępu do pól, aby pobrać wartości właściwości obiektu User.

W języku Kotlin właściwości są zwykle udostępniane za pomocą metod pobierających i ustawiających. Obejmuje to właściwości val.

Możesz zmienić to działanie za pomocą @JvmFieldadnotacji. Gdy ten modyfikator zostanie zastosowany do właściwości w klasie, Kotlin pominie generowanie metod pobierających (i ustawiających dla właściwości var), a pole zapasowe będzie dostępne bezpośrednio.

Obiekty User są niezmienne, dlatego chcemy udostępnić każdą z ich właściwości jako pole. W tym celu dodamy do każdej z nich adnotację @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
}

Jeśli teraz wrócimy do UseCase.formatUser(), zobaczymy, że błędy zostały naprawione.

@JvmField lub const

W pliku UseCase.java występuje podobny błąd:

Repository.saveAs(Repository.BACKUP_PATH);

Jeśli użyjemy autouzupełniania, zobaczymy, że jest tam Repository.getBACKUP_PATH(), więc może pojawić się pokusa, aby zmienić adnotację na BACKUP_PATH z @JvmStatic na @JvmField.

Spróbujmy. Wróć do Repository.kt i zaktualizuj adnotację:

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

Jeśli teraz spojrzymy na UseCase.java, zobaczymy, że błąd zniknął, ale na BACKUP_PATH jest też notatka:

W Kotlinie tylko typy pierwotne, takie jak int, floatString, mogą być const. W tym przypadku, ponieważ BACKUP_PATH jest ciągiem znaków, możemy uzyskać lepszą skuteczność, używając atrybutu const val zamiast atrybutu val z adnotacją @JvmField, zachowując przy tym możliwość uzyskania dostępu do wartości jako pola.

Zmieńmy to teraz w pliku Repository.kt:

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

Jeśli wrócimy do UseCase.java, zobaczymy, że pozostał tylko 1 błąd.

Ostateczny błąd to Exception: 'java.io.IOException' is never thrown in the corresponding try block.

Jeśli przyjrzymy się kodowi Repository.saveAsRepository.kt, zobaczymy, że zgłasza on wyjątek. Co się dzieje?

W Javie istnieje koncepcja „wyjątku sprawdzanego”. Są to wyjątki, które można naprawić, np. użytkownik wpisał nieprawidłową nazwę pliku lub sieć jest tymczasowo niedostępna. Po przechwyceniu wyjątku sprawdzanego deweloper może przekazać użytkownikowi informacje o tym, jak rozwiązać problem.

Wyjątki sprawdzane są sprawdzane w czasie kompilacji, dlatego deklarujesz je w sygnaturze metody:

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

Kotlin nie ma jednak wyjątków sprawdzanych, co jest przyczyną problemu.

Rozwiązaniem jest poproszenie Kotlina o dodanie potencjalnie zgłaszanego wyjątku IOException do sygnatury funkcji Repository.saveAs(), aby kod bajtowy JVM zawierał go jako wyjątek sprawdzany.

Robimy to za pomocą adnotacji @Throws w języku Kotlin, która ułatwia współdziałanie Javy i Kotlina. W Kotlinie wyjątki działają podobnie jak w Javie, ale w przeciwieństwie do Javy Kotlin ma tylko wyjątki niekontrolowane. Jeśli chcesz poinformować kod Java, że funkcja Kotlin zgłasza wyjątek, musisz użyć adnotacji @Throws w sygnaturze funkcji Kotlin. Przejdź do Repository.kt file i zaktualizuj saveAs(), aby uwzględnić nową adnotację:

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

Po dodaniu adnotacji @Throws widzimy, że wszystkie błędy kompilatora w UseCase.java zostały naprawione. Hurra!

Możesz się zastanawiać, czy podczas wywoływania funkcji saveAs() z Kotlinu musisz teraz używać bloków trycatch.

Nie. Pamiętaj, że Kotlin nie ma sprawdzanych wyjątków, a dodanie @Throws do metody tego nie zmienia:

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

Warto przechwytywać wyjątki, gdy można je obsłużyć, ale Kotlin nie wymusza ich obsługi.

W tym samouczku omówiliśmy podstawy pisania kodu w Kotlinie, który obsługuje też pisanie idiomatycznego kodu w Javie.

Omówiliśmy, jak za pomocą adnotacji możemy zmieniać sposób, w jaki Kotlin generuje kod bajtowy JVM, np.:

  • @JvmStatic, aby wygenerować statyczne elementy i metody.
  • @JvmOverloads do generowania przeciążonych metod dla funkcji, które mają wartości domyślne.
  • @JvmName – aby zmienić nazwy metod pobierających i ustawiających.
  • @JvmField, aby udostępnić właściwość bezpośrednio jako pole, a nie za pomocą metod pobierających i ustawiających.
  • @Throws, aby zadeklarować wyjątki sprawdzane.

Ostateczna zawartość naszych plików to:

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
}