Wywołanie kodu Kotlin z języka Java

Z tego modułu dowiesz się, jak pisać lub dostosowywać kod Kotlin, aby można go było łatwo wywołać za pomocą kodu Java.

Czego się nauczysz:

  • Jak używać adnotacji @JvmField, @JvmStatic i innych.
  • Ograniczenia dostępu do niektórych funkcji językowych Kotlin z kodu Java.

Co musisz wiedzieć

To ćwiczenie z programowania jest przeznaczone dla programistów i zakłada podstawową wiedzę w zakresie Javy i Kotlina.

To ćwiczenie funkcji programowania symuluje migrację większego projektu napisanego w języku Java, aby uwzględnić nowy kod Kotlin.

Dla ułatwienia stworzyliśmy 1 plik .java o nazwie UseCase.java, który reprezentuje istniejącą bazę kodu.

Wyobraźmy sobie, że właśnie zastąpiliśmy niektóre funkcje pierwotnie napisane w Javie nową wersją napisaną w Kotlin, i musimy dokończyć integrację.

Importowanie projektu

Kod projektu możesz skopiować z projektu na GitHubie: GitHub

Możesz także pobrać i wyodrębnić projekt z archiwum ZIP, które jest dostępne tutaj:

Pobierz aplikację Zip

Jeżeli używasz IntelliJ IDEA, wybierz &Importuj projekt&quot.

Jeśli korzystasz z Android Studio, wybierz „"Importuj projekt (Gradle, Eclipse ADT itp.)”.

Otwórzmy plik UseCase.java i rozpocznij pracę przy pojawianiu się błędów.

Pierwsza funkcja z problemem 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 zarówno w przypadku Repository.getNextGuestId(), jak i Repository.addUser(...) są takie same: "Nie statyczny jest niedostępny w kontekście statycznym."

Teraz spójrzmy na jeden z plików Kotlin. Otwórz plik Repository.kt.

Widzimy, że nasze Repozytorium jest pojedyncze, które jest zadeklarowane przez użycie słowa kluczowego „object”. Problem polega na tym, że Kotlin generuje w obrębie naszej klasy statyczną instancję, zamiast ujawniać je jako właściwości statyczne i metody.

Na przykład do określenia Repository.getNextGuestId() można się odwoływać, używając właściwości Repository.INSTANCE.getNextGuestId(), ale jest to lepszy sposób.

Możemy generować Kotlin, generując statyczne metody i właściwości, oznaczając publiczne właściwości i metody repozytorium @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)
   }
}

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

Jeśli przełączymy się z powrotem na UseCase.java, usługi i metody w usłudze Repository nie będą już powodować błędów (z wyjątkiem Repository.BACKUP_PATH). Wrócimy do tego później.

Na razie poprawmy następny błąd w metodzie registerGuest().

Załóżmy, że mamy klasę StringUtils z kilkoma statycznymi funkcjami do obsługi ciągów znaków. Po przekonwertowaniu na format Kotlin przekształciliśmy metody w funkcje rozszerzeń. Java nie ma funkcji rozszerzeń, więc Kotlin kompiluje te metody jako funkcje statyczne.

Jeśli spojrzymy na metodę registerGuest() w systemie UseCase.java, zobaczysz, że coś jest nie w porządku:

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

Dzieje się tak, ponieważ Kotlin umieszcza funkcje " najwyższego poziomu" lub funkcje na poziomie pakietów w klasie, których nazwa jest oparta na nazwie pliku. W tym przypadku, ponieważ plik ma nazwę StringUtils.kt, odpowiednia klasa ma nazwę StringUtilsKt.

Mogliśmy zmienić wszystkie odniesienia do StringUtils na StringUtilsKt i naprawić ten błąd, ale nie jest to idealne rozwiązanie, ponieważ:

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

Dlatego zamiast zmieniać kod w języku Java, zaktualizuj kod Kotlin, tak aby korzystał z innej nazwy dla tych metod.

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

package com.google.example.javafriendlykotlin

Możesz poinformować Kotlin, aby użyć innej nazwy dla metod na poziomie pakietu, korzystając z adnotacji @file:JvmName. Użyjemy tej adnotacji do nazwania klasy StringUtils.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Kiedy teraz przyjrzymy się adresowi UseCase.java, zobaczysz, że błąd o nazwie StringUtils.nameToLogin() został rozwiązany.

Niestety ten błąd został zastąpiony nowym o parametrach przekazywanych do konstruktora dla parametru User. Przejdźmy do następnego kroku i napraw ostatni błąd w aplikacji UseCase.registerGuest().

Kotlin obsługuje domyślne wartości parametrów. Spójrz na blok init, w którym widać, jak jest używany. Repository.kt

Repository.kt:

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

Jak widać, możemy pominąć wartość dla użytkownika displayName, bo jest to domyślna wartość podana w User.kt.

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 w przypadku wywoływania metody 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 rozwiązać ten problem, pozwól Google Kotlin wygenerować przeciążenia naszego konstruktora za pomocą adnotacji @JvmOverloads.

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

Klasa User ma tylko jeden konstruktor główny. Konstruktor nie zawiera adnotacji, więc słowo kluczowe constructor zostało pominięte. Teraz chcemy dodać do nich adnotacje, ale trzeba 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")
)

Mając 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")
)

Po powrocie do UseCase.java zauważyliśmy, że nie ma już więcej błędów w funkcji registerGuest.

Następnym krokiem jest naprawienie uszkodzonego połączenia z numerem user.hasSystemAccess() w aplikacji UseCase.getSystemUsers(). Przejdź do następnego kroku. Możesz też przeczytać więcej informacji o tym, jak @JvmOverloads naprawił błąd.

@JvmOverloads

Aby lepiej zrozumieć, co robi @JvmOverloads, utwórz 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 z 2 parametrami, id i username:

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

Możesz też utworzyć User, dodając trzeci parametr dla displayName, nadal korzystając z wartości domyślnej groups:

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

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

Usuń więc ten wiersz lub poprzedź go prefiksem „//&#39”.

Jeśli w Kotlinie chcesz połączyć parametry domyślne i niedomyślne, musisz użyć parametrów z nazwą.

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

Kotlin wygeneruje przeciążenia funkcji, w tym konstrukcje, ale utworzy tylko jedno przeciążenie na parametr z wartością domyślną.

Przyjrzyjmy się temu elementowi UseCase.java i rozwiążmy następny problem: wywołanie metody 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 interesujący błąd. Jeśli używasz funkcji autouzupełniania w IDE&#39 w klasie User, zauważysz, że nazwa użytkownika hasSystemAccess() została zmieniona na getHasSystemAccess().

Aby rozwiązać ten problem, chcemy, aby Kotlin wygenerował inną nazwę dla właściwości val hasSystemAccess. Do tego celu używamy adnotacji @JvmName. Wróć do aplikacji User.kt i sprawdź, gdzie ją stosować.

Adnotację można umieścić na 2 sposoby. Pierwszy to zastosowanie bezpośrednio do metody get():

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

Informuje to Kotlin o konieczności zmiany podpisu wyraźnie wskazanego obiektu na nazwę.

Możesz też zastosować ją do usługi, używając prefiksu get: w ten sposób:

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

Ta metoda jest szczególnie przydatna w przypadku usług, które korzystają z domyślnej, zdefiniowanej domyślnie metody getter. Przykład:

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

Dzięki temu można zmienić nazwę getter&#39' bez dokładnego definiowania metody getter.

Bez względu na to, co robisz, możesz używać, co uważasz za lepsze. W obu przypadkach Kotlin utworzy getter o nazwie hasSystemAccess().

Jeśli przełączymy się z powrotem na UseCase.java, możemy potwierdzić, że domena getSystemUsers() jest wolna od błędów.

Kolejny błąd dotyczy formatUser(), ale jeśli chcesz dowiedzieć się więcej o konwencji nazewnictwa gett Kotlin, przeczytaj ten artykuł tutaj, zanim przejdziesz do następnego kroku.

Nazwy getterów i seterów

Kiedy piszemy Kotlin, można łatwo zapomnieć, że pisanie kodu to np.:

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

Zwraca funkcję o wartości displayName. Aby to sprawdzić, kliknij w menu Narzędzia > Kotlin > Show Kotlin Bytecode, a następnie kliknij przycisk Dekompiluj:

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

Jeśli chcemy uzyskać do nich dostęp z użyciem języka Java, musimy jednoznacznie zapisać nazwę gettera.

W większości przypadków nazwy Java elementów pobierających w usługach Kotlin to get + nazwa usługi, tak jak wykryliśmy w polach User.getHasSystemAccess() i User.getDisplayName(). Jedynym wyjątkiem są usługi, których nazwy zaczynają się od „"is"”. W tym przypadku nazwa Java gettera to nazwa usługi Kotlin.

Na przykład właściwość User w rodzaju:

val isAdmin get() = //...

Uzyskany będzie dostęp z Javy z:

boolean userIsAnAdmin = user.isAdmin();

Używając adnotacji @JvmName, Kotlin generuje kod bajtowy o podanej nazwie, a nie domyślnej, do elementu.

Działa to tak samo w przypadku elementów ustawiających, których generowane nazwy to zawsze set + nazwa właściwości. Na przykład wybierz takie zajęcia:

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

Wyobraźmy sobie, że chcemy zmienić nazwę zestawu z „setRed()” na „updateRed()”, a my nie będziemy już prosić o pomoc. W tym celu używamy wersji @set:JvmName:

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

Dzięki Javie możemy pisać:

color.updateRed(0.8f);

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

W Kotlinie właściwości są zwykle udostępniane przez metody getter i setery. Dotyczy to val usług.

Można to zmienić za pomocą adnotacji @JvmField. W przypadku tego ustawienia, który zostanie zastosowany w usłudze w klasie, Kotlin pominie generowanie metod getter (i kodu ustawiającego w przypadku usług var), a pole backendu jest dostępne bezpośrednio.

Obiekty User są stałe, chcemy więc udostępniać każdą z nich w postaci pól, więc o nich dowiemy się za pomocą atrybutu @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
}

Kiedy teraz wyświetlimy zapytanie UseCase.formatUser(), zobaczysz, że błędy zostały naprawione!

@JvmField lub Const

Pojawia się też kolejny podobny błąd w pliku UseCase.java:

Repository.saveAs(Repository.BACKUP_PATH);

Jeśli użyjemy tu autouzupełniania, widzimy, że wartość Repository.getBACKUP_PATH() jest ustawiona, dlatego zmiana adnotacji BACKUP_PATH na @JvmStatic z @JvmField może być kusząca.

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

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

Jeśli teraz przyjrzymy się problemowi UseCase.java, zauważymy, że błąd zniknął, ale jest też informacja o BACKUP_PATH:

W Kotlinie jedynymi atrybutami, które mogą być const, są elementy podstawowe, takie jak int, float i String. W tym przypadku, ponieważ ciąg BACKUP_PATH jest ciągiem, możemy uzyskać lepszą wydajność, używając tagu const val zamiast val z adnotacjami @JvmField, zachowując przy tym możliwość dostępu do wartości jako pola.

Zmieńmy to w Repository.kt:

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

Jeśli przyjrzymy się adresowi UseCase.java, zobaczymy tylko jeden błąd.

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

Jeśli jednak przyjrzymy się kodowi Repository.saveAs w polu Repository.kt, zauważyliśmy, że jest to wyjątek. Co jest nie tak?

W Javie istnieje koncepcja „ zaznaczonego wyjątku”. Są to wyjątki, od których można odzyskać adresy URL, takie jak nieprawidłowy zapis nazwy użytkownika lub tymczasowo niedostępna sieć. Po wykryciu tego wyjątku deweloper może poinformować użytkownika, jak rozwiązać problem.

Ponieważ wyjątki zaznaczone są sprawdzane podczas kompilowania, należy je zadeklarować w podpisie metody:

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

Z kolei Kotlin nie ma żadnych wyjątków, a to powoduje ten problem.

Rozwiązaniem jest poproszenie Kotlina o dodanie IOException, które potencjalnie jest rejestrowane do podpisu znacznika Repository.saveAs(), tak aby kod bajtowy JVM zawierał go jako sprawdzany wyjątek.

Posługujemy się adnotacją Kotlin @Throws, która ułatwia współdziałanie z językami Java i Kotlin. W Kotlinie wyjątki są podobne do języka Java, ale w przeciwieństwie do niego Kotlin ma tylko wyjątki. Jeśli więc chcesz powiadomić kod Java, że funkcja Kotlin robi wyjątek, musisz użyć adnotacji @Throws do podpisu funkcji Kotlin. Przełącz się na 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 kompilacji w narzędziu UseCase.java zostały naprawione. Hurra!

Być może zastanawiasz się, czy obecnie, gdy zadzwonisz do firmy saveAs() z Kotlin, musisz użyć bloków try i catch.

Nie. Pamiętaj, że Kotlin nie ma wyjątków i nie dodaje @Throws do metody. Nie zmienia to tego:

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

Nadal warto wykrywać wyjątki, gdy można je obsługiwać, ale Kotlin nie zmusi Cię do ich obsługi.

W tym module omówiliśmy podstawy tworzenia kodu Kotlin, który obsługuje również pisanie idiomatycznego kodu Java.

Omówiliśmy sposób korzystania z adnotacji, by zmienić sposób generowania kodu bajtowego JVM w Kotlin, na przykład:

  • @JvmStatic, aby wygenerować statycznych członków i metody.
  • @JvmOverloads, aby wygenerować przeciążone metody dotyczące funkcji, które mają wartości domyślne.
  • @JvmName, aby zmienić nazwy pobierających i osób określających.
  • @JvmField, aby wyświetlać właściwość bezpośrednio jako pole, a nie przez metody getter i setery.
  • @Throws, aby zgłosić zaznaczone wyjątki.

Ostateczna zawartość 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
}