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:
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()
i 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()
w 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:JvmName
adnotacji. 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 init
w Repository.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: id
i username
:
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 val
hasSystemAccess
. 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ą @JvmField
adnotacji. 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
, float
i String
, 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.saveAs
w Repository.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 try
i catch
.
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
}