Dalam codelab ini, Anda akan mempelajari cara menulis atau menyesuaikan kode Kotlin agar lebih mudah dipanggil dari kode Java.
Yang akan Anda pelajari
- Cara menggunakan
@JvmField
,@JvmStatic
, dan anotasi lainnya. - Batasan dalam mengakses fitur bahasa Kotlin tertentu dari kode Java.
Yang harus sudah Anda ketahui
Codelab ini ditulis untuk programmer dan mengasumsikan pengetahuan dasar Java dan Kotlin.
Codelab ini menyimulasikan migrasi sebagian project yang lebih besar yang ditulis dengan bahasa pemrograman Java, untuk menggabungkan kode Kotlin baru.
Untuk menyederhanakan, kita akan memiliki satu file .java
bernama UseCase.java
, yang akan merepresentasikan codebase yang ada.
Kita akan membayangkan bahwa kita baru saja mengganti beberapa fungsi yang awalnya ditulis dalam Java dengan versi baru yang ditulis dalam Kotlin, dan kita perlu menyelesaikan integrasinya.
Mengimpor project
Kode project dapat di-clone dari project GitHub di sini: GitHub
Atau, Anda dapat mendownload dan mengekstrak project dari arsip zip yang ada di sini:
Jika Anda menggunakan IntelliJ IDEA, pilih "Import Project".
Jika Anda menggunakan Android Studio, pilih "Import project (Gradle, Eclipse ADT, dll.)".
Mari kita buka UseCase.java
dan mulai mengatasi error yang kita lihat.
Fungsi pertama yang bermasalah adalah registerGuest
:
public static User registerGuest(String name) {
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Repository.addUser(guest);
return guest;
}
Error untuk Repository.getNextGuestId()
dan Repository.addUser(...)
sama: "Non-static cannot be accessed from a static context".
Sekarang mari kita lihat salah satu file Kotlin. Buka file Repository.kt
.
Kita melihat bahwa Repository kita adalah singleton yang dideklarasikan menggunakan kata kunci object. Masalahnya adalah Kotlin membuat instance statis di dalam class kita, bukan mengeksposnya sebagai properti dan metode statis.
Misalnya, Repository.getNextGuestId()
dapat dirujuk menggunakan Repository.INSTANCE.getNextGuestId()
, tetapi ada cara yang lebih baik.
Kita dapat membuat Kotlin menghasilkan metode dan properti statis dengan menganotasi properti dan metode publik Repository dengan @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)
}
}
Tambahkan anotasi @JvmStatic ke kode Anda menggunakan IDE.
Jika kita beralih kembali ke UseCase.java
, properti dan metode di Repository
tidak lagi menyebabkan error, kecuali Repository.BACKUP_PATH
. Kita akan membahasnya nanti.
Untuk saat ini, mari kita perbaiki error berikutnya dalam metode registerGuest()
.
Pertimbangkan skenario berikut: kita memiliki class StringUtils
dengan beberapa fungsi statis untuk operasi string. Saat mengonversinya ke Kotlin, kita mengonversi metode menjadi fungsi ekstensi. Java tidak memiliki fungsi ekstensi, jadi Kotlin mengompilasi metode ini sebagai fungsi statis.
Sayangnya, jika kita melihat metode registerGuest()
di dalam UseCase.java
, kita dapat melihat bahwa ada sesuatu yang tidak beres:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Alasannya adalah Kotlin menempatkan fungsi "level atas" atau level paket ini di dalam class yang namanya didasarkan pada nama file. Dalam hal ini, karena file diberi nama StringUtils.kt, class yang sesuai diberi nama StringUtilsKt
.
Kita dapat mengubah semua referensi StringUtils
menjadi StringUtilsKt
dan memperbaiki error ini, tetapi hal ini tidak ideal karena:
- Mungkin ada banyak tempat dalam kode kita yang perlu diperbarui.
- Nama itu sendiri aneh.
Jadi, daripada memfaktorkan ulang kode Java, mari perbarui kode Kotlin untuk menggunakan nama yang berbeda untuk metode ini.
Buka StringUtils.Kt
, dan cari deklarasi paket berikut:
package com.google.example.javafriendlykotlin
Kita dapat memberi tahu Kotlin untuk menggunakan nama yang berbeda untuk metode tingkat paket dengan menggunakan anotasi @file:JvmName
. Mari kita gunakan anotasi ini untuk memberi nama class StringUtils
.
@file:JvmName("StringUtils")
package com.google.example.javafriendlykotlin
Sekarang, jika kita melihat kembali UseCase.java
, kita dapat melihat bahwa error untuk StringUtils.nameToLogin()
telah diselesaikan.
Sayangnya, error ini diganti dengan error baru tentang parameter yang diteruskan ke konstruktor untuk User
. Lanjutkan ke langkah berikutnya dan perbaiki error terakhir ini di UseCase.registerGuest()
.
Kotlin mendukung nilai default untuk parameter. Kita dapat melihat cara penggunaannya dengan melihat di dalam blok init
dari Repository.kt
.
Repository.kt:
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
Kita dapat melihat bahwa untuk pengguna "warlow", kita dapat melewati pengisian nilai untuk displayName
karena ada nilai default yang ditentukan untuknya di User.kt
.
User.kt:
data class User(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
Sayangnya, hal ini tidak berfungsi sama saat memanggil metode dari Java.
UseCase.java:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Nilai default tidak didukung dalam bahasa pemrograman Java. Untuk memperbaikinya, mari beri tahu Kotlin agar membuat kelebihan beban untuk konstruktor kita dengan bantuan anotasi @JvmOverloads.
Pertama, kita harus melakukan sedikit pembaruan pada User.kt
.
Karena class User
hanya memiliki satu konstruktor utama, dan konstruktor tidak menyertakan anotasi apa pun, kata kunci constructor
telah dihilangkan. Namun, karena kita ingin memberikan anotasi, kata kunci constructor
harus disertakan:
data class User constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
Dengan adanya kata kunci constructor
, kita dapat menambahkan anotasi @JvmOverloads
:
data class User @JvmOverloads constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
Jika kita beralih kembali ke UseCase.java
, kita dapat melihat bahwa tidak ada lagi error dalam fungsi registerGuest
.
Langkah berikutnya adalah memperbaiki panggilan yang rusak ke user.hasSystemAccess()
di UseCase.getSystemUsers()
. Lanjutkan ke langkah berikutnya untuk melakukannya, atau lanjutkan membaca untuk mempelajari lebih lanjut tindakan yang telah dilakukan @JvmOverloads
untuk memperbaiki error tersebut.
@JvmOverloads
Untuk lebih memahami fungsi @JvmOverloads
, mari kita buat metode pengujian di 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);
}
Kita dapat membuat User
hanya dengan dua parameter, id
dan username
:
User syrinx = new User(1001, "syrinx");
Kita juga dapat membuat User
dengan menyertakan parameter ketiga untuk displayName
sambil tetap menggunakan nilai default untuk groups
:
User ione = new User(1002, "ione", "Ione Saldana");
Namun, displayName
tidak dapat dilewati dan hanya memberikan nilai untuk groups
tanpa menulis kode tambahan:
Jadi, hapus baris tersebut atau tambahkan ‘//' di depannya untuk menjadikannya sebagai komentar.
Di Kotlin, jika ingin menggabungkan parameter default dan non-default, kita perlu menggunakan parameter bernama.
// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))
Alasannya adalah Kotlin akan membuat overload untuk fungsi, termasuk konstruktor, tetapi hanya akan membuat satu overload per parameter dengan nilai default.
Mari kita lihat kembali UseCase.java
dan atasi masalah berikutnya: panggilan ke user.hasSystemAccess()
dalam metode 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;
}
Ini adalah error yang menarik. Jika Anda menggunakan fitur pelengkapan otomatis IDE pada class User
, Anda akan melihat bahwa hasSystemAccess()
diganti namanya menjadi getHasSystemAccess()
.
Untuk memperbaiki masalah ini, kita ingin Kotlin membuat nama yang berbeda untuk properti val
hasSystemAccess
. Untuk melakukannya, kita dapat menggunakan anotasi @JvmName
. Mari kita beralih kembali ke User.kt
dan lihat di mana kita harus menerapkannya.
Ada dua cara untuk menerapkan anotasi. Yang pertama adalah menerapkannya langsung ke metode get()
, seperti ini:
val hasSystemAccess
@JvmName("hasSystemAccess")
get() = "sys" in groups
Hal ini memberi sinyal ke Kotlin untuk mengubah tanda tangan getter yang ditentukan secara eksplisit ke nama yang diberikan.
Atau, Anda dapat menerapkannya ke properti dengan menggunakan awalan get:
seperti ini:
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
Metode alternatif ini sangat berguna untuk properti yang menggunakan getter yang ditentukan secara implisit dan default. Contoh:
@get:JvmName("isActive")
val active: Boolean
Hal ini memungkinkan nama getter diubah tanpa harus menentukan getter secara eksplisit.
Meskipun ada perbedaan ini, Anda dapat menggunakan mana pun yang terasa lebih baik bagi Anda. Keduanya akan menyebabkan Kotlin membuat getter dengan nama hasSystemAccess()
.
Jika kita beralih kembali ke UseCase.java
, kita dapat memverifikasi bahwa getSystemUsers()
kini bebas error.
Error berikutnya ada di formatUser()
, tetapi jika Anda ingin membaca selengkapnya tentang konvensi penamaan getter Kotlin, lanjutkan membaca di sini sebelum melanjutkan ke langkah berikutnya.
Penamaan Pengambil dan Penyetel
Saat menulis Kotlin, kita sering lupa bahwa menulis kode seperti:
val myString = "Logged in as ${user.displayName}")
Sebenarnya memanggil fungsi untuk mendapatkan nilai displayName
. Kita dapat memverifikasinya dengan membuka Tools > Kotlin > Show Kotlin Bytecode di menu, lalu mengklik tombol Decompile:
String myString = "Logged in as " + user.getDisplayName();
Jika ingin mengaksesnya dari Java, kita harus menulis nama getter secara eksplisit.
Dalam sebagian besar kasus, nama Java untuk getter properti Kotlin hanyalah get
+ nama properti, seperti yang telah kita lihat dengan User.getHasSystemAccess()
dan User.getDisplayName()
. Satu-satunya pengecualian adalah properti yang namanya diawali dengan "is". Dalam hal ini, nama Java untuk getter adalah nama properti Kotlin.
Misalnya, properti di User
seperti:
val isAdmin get() = //...
Akan diakses dari Java dengan:
boolean userIsAnAdmin = user.isAdmin();
Dengan menggunakan anotasi @JvmName
, Kotlin menghasilkan bytecode yang memiliki nama yang ditentukan, bukan nama default, untuk item yang diberi anotasi.
Hal ini berfungsi sama untuk setter, yang nama yang dibuatnya selalu set
+ nama properti. Misalnya, perhatikan class berikut:
class Color {
var red = 0f
var green = 0f
var blue = 0f
}
Bayangkan kita ingin mengubah nama penyetel dari setRed()
menjadi updateRed()
, tanpa mengubah pengambil. Kita dapat menggunakan versi @set:JvmName
untuk melakukannya:
class Color {
@set:JvmName("updateRed")
var red = 0f
@set:JvmName("updateGreen")
var green = 0f
@set:JvmName("updateBlue")
var blue = 0f
}
Dari Java, kita kemudian dapat menulis:
color.updateRed(0.8f);
UseCase.formatUser()
menggunakan akses kolom langsung untuk mendapatkan nilai properti objek User
.
Di Kotlin, properti biasanya diekspos melalui pengambil dan penyetel. Hal ini mencakup properti val
.
Perilaku ini dapat diubah dengan menggunakan anotasi @JvmField
. Jika diterapkan ke properti dalam class, Kotlin akan melewati pembuatan metode getter (dan setter untuk properti var
), dan kolom pendukung dapat diakses secara langsung.
Karena objek User
tidak dapat diubah, kita ingin menampilkan setiap propertinya sebagai kolom, sehingga kita akan menganotasi setiap properti dengan @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
}
Jika kita melihat kembali UseCase.formatUser()
, kita dapat melihat bahwa error telah diperbaiki.
@JvmField atau const
Dengan begitu, ada error lain yang terlihat serupa dalam file UseCase.java
:
Repository.saveAs(Repository.BACKUP_PATH);
Jika kita menggunakan isi otomatis di sini, kita dapat melihat bahwa ada Repository.getBACKUP_PATH()
, sehingga kita mungkin tergoda untuk mengubah anotasi pada BACKUP_PATH
dari @JvmStatic
menjadi @JvmField
.
Mari kita coba. Beralih kembali ke Repository.kt
, dan perbarui anotasi:
object Repository {
@JvmField
val BACKUP_PATH = "/backup/user.repo"
Jika kita melihat UseCase.java
sekarang, kita akan melihat bahwa errornya sudah hilang, tetapi ada juga catatan di BACKUP_PATH
:
Di Kotlin, satu-satunya jenis yang dapat berupa const
adalah primitif, seperti int
, float
, dan String
. Dalam hal ini, karena BACKUP_PATH
adalah string, kita bisa mendapatkan performa yang lebih baik dengan menggunakan const val
, bukan val
yang diberi anotasi dengan @JvmField
, sekaligus mempertahankan kemampuan untuk mengakses nilai sebagai kolom.
Mari kita ubah sekarang di Repository.kt:
object Repository {
const val BACKUP_PATH = "/backup/user.repo"
Jika kita melihat kembali UseCase.java
, kita dapat melihat bahwa hanya ada satu error yang tersisa.
Error terakhir menyatakan Exception: 'java.io.IOException' is never thrown in the corresponding try block.
Jika kita melihat kode untuk Repository.saveAs
di Repository.kt
, kita akan melihat bahwa kode tersebut memang memunculkan pengecualian. Apa yang terjadi?
Java memiliki konsep "pengecualian yang diperiksa". Ini adalah pengecualian yang dapat dipulihkan, seperti pengguna salah mengetik nama file, atau jaringan tidak tersedia untuk sementara. Setelah pengecualian yang diperiksa ditangkap, developer kemudian dapat memberikan masukan kepada pengguna tentang cara memperbaiki masalah tersebut.
Karena pengecualian yang diperiksa diperiksa pada waktu kompilasi, Anda mendeklarasikannya dalam tanda tangan metode:
public void openFile(File file) throws FileNotFoundException {
// ...
}
Di sisi lain, Kotlin tidak memiliki pengecualian yang diperiksa, dan itulah yang menyebabkan masalah di sini.
Solusinya adalah meminta Kotlin untuk menambahkan IOException
yang berpotensi dilempar ke tanda tangan Repository.saveAs()
, sehingga bytecode JVM menyertakannya sebagai pengecualian yang diperiksa.
Kita melakukannya dengan anotasi @Throws
Kotlin, yang membantu interoperabilitas Java/Kotlin. Di Kotlin, pengecualian berperilaku mirip dengan Java, tetapi tidak seperti Java, Kotlin hanya memiliki pengecualian yang tidak diperiksa. Jadi, jika Anda ingin memberi tahu kode Java bahwa fungsi Kotlin memunculkan pengecualian, Anda harus menggunakan anotasi @Throws ke tanda tangan fungsi Kotlin. Beralihlah ke Repository.kt file
dan perbarui saveAs()
untuk menyertakan anotasi baru:
@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...
}
Dengan adanya anotasi @Throws
, kita dapat melihat bahwa semua error compiler di UseCase.java
telah diperbaiki. Hore!
Anda mungkin bertanya-tanya apakah Anda harus menggunakan blok try
dan catch
saat memanggil saveAs()
dari Kotlin sekarang.
Tidak. Ingat, Kotlin tidak memiliki pengecualian yang diperiksa, dan menambahkan @Throws
ke metode tidak mengubahnya:
fun saveFromKotlin(path: String) {
Repository.saveAs(path)
}
Menangkap pengecualian masih berguna jika pengecualian dapat ditangani, tetapi Kotlin tidak mengharuskan Anda menanganinya.
Dalam codelab ini, kita telah membahas dasar-dasar cara menulis kode Kotlin yang juga mendukung penulisan kode Java idiomatis.
Kita telah membahas cara menggunakan anotasi untuk mengubah cara Kotlin membuat bytecode JVM, seperti:
@JvmStatic
untuk menghasilkan anggota dan metode statis.@JvmOverloads
untuk menghasilkan metode yang kelebihan beban untuk fungsi yang memiliki nilai default.@JvmName
untuk mengubah nama pengambil dan penyetel.@JvmField
untuk mengekspos properti secara langsung sebagai kolom, bukan melalui pengambil dan penyetel.@Throws
untuk mendeklarasikan pengecualian yang diperiksa.
Isi akhir file kita adalah:
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
}