Memanggil Kode Kotlin dari Java

Dalam codelab ini, Anda akan mempelajari cara menulis atau menyesuaikan kode Kotlin agar lebih mudah dipanggil dari kode Java.

Yang akan Anda pelajari

  • Cara memanfaatkan @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 bagian project lebih besar yang ditulis dengan bahasa pemrograman Java, untuk menggabungkan kode Kotlin baru.

Untuk menyederhanakannya, kita akan memiliki satu file .java bernama UseCase.java, yang akan mewakili codebase yang ada.

Kami akan membayangkan bahwa kami baru saja mengganti beberapa fungsi yang awalnya ditulis dalam Java dengan versi baru yang ditulis dalam Kotlin, dan kami harus mengintegrasikannya.

Mengimpor project

Kode project dapat di-clone dari project GitHub di sini: GitHub

Atau, Anda dapat mendownload dan mengekstrak project dari arsip zip yang ditemukan di sini:

Download Zip

Jika Anda menggunakan IntelliJ IDEA, pilih "Import Project".

Jika Anda menggunakan Android Studio, pilih "Import project (Gradle, Eclipse ADT, etc.)".

Mari buka UseCase.java dan mulailah menangani error yang kita lihat.

Fungsi pertama yang memiliki masalah 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 tidak dapat diakses dari konteks statis."

Sekarang mari kita lihat salah satu file Kotlin. Buka file Repository.kt.

Kita melihat bahwa Repositori adalah singleton yang dideklarasikan menggunakan kata kunci objek. Masalahnya adalah Kotlin menghasilkan instance statis di dalam class, bukan mengeksposnya sebagai properti dan metode statis.

Misalnya, Repository.getNextGuestId() dapat direferensikan menggunakan Repository.INSTANCE.getNextGuestId(), tetapi ada cara yang lebih baik.

Kita dapat membuat Kotlin menghasilkan metode dan properti statis dengan menganotasi properti publik dan metode Repositori 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 untuk Repository.BACKUP_PATH. Kita akan membahasnya lagi nanti.

Untuk saat ini, mari kita perbaiki error berikutnya dalam metode registerGuest().

Mari kita bahas skenario berikut: kita memiliki class StringUtils dengan beberapa fungsi statis untuk operasi string. Saat mengonversinya ke Kotlin, kami mengonversi metode menjadi fungsi ekstensi. Java tidak memiliki fungsi ekstensi, sehingga Kotlin mengompilasi metode ini sebagai fungsi statis.

Sayangnya, jika kita melihat metode registerGuest() di dalam UseCase.java, kita dapat melihat bahwa ada sesuatu yang salah:

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

Alasannya adalah Kotlin menempatkan fungsi "top-level" 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 akan diberi nama StringUtilsKt.

Kita dapat mengubah semua referensi StringUtils menjadi StringUtilsKt dan memperbaiki error ini, tetapi ini tidak ideal karena:

  • Mungkin ada banyak tempat di kode kita yang perlu diperbarui.
  • Nama itu sendiri canggung.

Jadi, daripada memfaktorkan ulang kode Java, mari kita perbarui kode Kotlin agar 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 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 melihat kembali UseCase.java, kita dapat melihat bahwa error untuk StringUtils.nameToLogin() telah diatasi.

Sayangnya, error ini digantikan dengan error baru tentang parameter yang diteruskan ke konstruktor untuk User. Mari lanjutkan ke langkah berikutnya dan memperbaiki error terakhir ini di UseCase.registerGuest().

Kotlin mendukung nilai default untuk parameter. Kita dapat melihat cara penggunaannya dengan melihat di dalam blok init 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 &wart", kita dapat melewati nilai untuk displayName karena ada nilai default yang ditentukan untuk nilai tersebut dalam 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, cara 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, beri tahu Kotlin untuk menghasilkan overload bagi konstruktor 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, setelah kami menganotasinya, 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 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 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 itu, atau lanjutkan membaca untuk mengetahui lebih dalam apa yang telah dilakukan @JvmOverloads untuk memperbaiki error.

@JvmOverloads

Untuk lebih memahami fungsi @JvmOverloads, mari 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 sembari tetap menggunakan nilai default untuk groups:

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

Namun, Anda tidak dapat melewati displayName dan hanya memberikan nilai untuk groups tanpa menulis kode tambahan:

Jadi, mari hapus baris tersebut atau awali dengan ‘//' untuk mengomentarinya.

Di Kotlin, jika ingin menggabungkan parameter default dan non-default, kita harus 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 menghasilkan overload untuk fungsi, termasuk konstruktor, tetapi hanya akan membuat satu overload per parameter dengan nilai default.

Mari lihat kembali UseCase.java dan tangani 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 di class User, Anda akan melihat bahwa hasSystemAccess() diganti namanya menjadi getHasSystemAccess().

Untuk memperbaiki masalah ini, kami ingin Kotlin membuat nama yang berbeda untuk properti val hasSystemAccess. Untuk melakukannya, kita dapat menggunakan anotasi @JvmName. Mari beralih kembali ke User.kt dan melihat 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

Tindakan ini memberikan sinyal ke Kotlin untuk mengubah tanda tangan pengambil 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 sangat berguna untuk properti yang menggunakan pengambil default yang ditentukan secara implisit. Misalnya:

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

Hal ini memungkinkan nama pengambil diubah tanpa harus menentukan pengambil secara eksplisit.

Terlepas dari perbedaan ini, Anda dapat menggunakan yang menurut Anda lebih baik. Keduanya akan menyebabkan Kotlin membuat pengambil dengan nama hasSystemAccess().

Jika 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 pengambil Kotlin, lanjutkan membaca di sini sebelum melanjutkan ke langkah berikutnya.

Penamaan Pengambil dan Penyetel

Saat kita menulis Kotlin, kita mudah lupa bahwa menulis kode seperti:

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

Memanggil fungsi untuk mendapatkan nilai displayName. Kita dapat memverifikasi hal ini dengan membuka Tools > Kotlin > Show Kotlin Bytecode pada menu, lalu mengklik tombol Decompile:

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

Jika ingin mengaksesnya dari Java, kita perlu secara eksplisit menuliskan nama pengambil.

Biasanya, nama Java pengambil untuk properti Kotlin hanya get + nama properti, seperti yang kita lihat dengan User.getHasSystemAccess() dan User.getDisplayName(). Satu-satunya pengecualian adalah properti yang namanya diawali dengan "is". Dalam hal ini, nama Java untuk pengambil 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 membuat bytecode yang memiliki nama tertentu, bukan yang default, untuk item yang dianotasi.

Ini berfungsi sama untuk penyetel, yang nama yang dihasilkan selalu set + nama properti. Misalnya, ambil class berikut:

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

Mari kita bayangkan kita ingin mengubah nama penyetel dari setRed() menjadi updateRed(), sembari tidak menggunakan pengambil. Kita dapat menggunakan versi @set:JvmName untuk melakukan hal ini:

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. Ini termasuk properti val.

Perilaku ini dapat diubah menggunakan anotasi @JvmField. Saat ini diterapkan ke properti di class, Kotlin akan melewati pembuatan metode pengambil (dan penyetel untuk properti var), dan kolom pendukung dapat diakses secara langsung.

Karena objek User tidak dapat diubah, kami ingin menampilkan setiap propertinya sebagai kolom. Jadi, kami akan memberi anotasi pada setiap properti tersebut 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 melihat kembali UseCase.formatUser() sekarang, kita dapat melihat bahwa error telah diperbaiki.

@JvmField atau const

Dengan itu, ada error lain yang mirip dalam file UseCase.java:

Repository.saveAs(Repository.BACKUP_PATH);

Jika kita menggunakan pelengkapan otomatis di sini, kita dapat melihat bahwa ada Repository.getBACKUP_PATH(), sehingga mungkin tergoda untuk mengubah anotasi di BACKUP_PATH dari @JvmStatic menjadi @JvmField.

Mari kita coba ini. 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 error tersebut sudah hilang, tetapi ada juga catatan tentang 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 daripada val yang dianotasikan dengan @JvmField, dengan tetap mempertahankan kemampuan untuk mengakses nilai sebagai kolom.

Mari kita ubah sekarang di Repository.kt:

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

Jika menilik ke belakang UseCase.java, kita hanya bisa melihat bahwa hanya ada satu error.

Error terakhir mengatakan Exception: 'java.io.IOException' is never thrown in the corresponding try block.

Jika melihat kode untuk Repository.saveAs di Repository.kt, kita akan melihat bahwa kode tersebut menampilkan 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 terdeteksi, developer kemudian dapat memberikan masukan kepada pengguna tentang cara memperbaiki masalah.

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 memeriksa pengecualian, dan itulah yang menyebabkan masalah di sini.

Solusinya adalah meminta Kotlin untuk menambahkan IOException yang berpotensi dilemparkan ke tanda tangan Repository.saveAs(), sehingga bytecode JVM menyertakannya sebagai pengecualian yang dicentang.

Kami 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 dicentang. Jadi, jika Anda ingin memberi tahu kode Java bahwa fungsi Kotlin melempar pengecualian, Anda harus menggunakan anotasi @Throws ke tanda tangan fungsi Kotlin Beralih ke Repository.kt file dan memperbarui 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 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 memeriksa pengecualian, dan menambahkan @Throws ke metode tidak mengubah hal tersebut:

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

Masih berguna untuk menangkap pengecualian ketika pengecualian dapat ditangani, tetapi Kotlin tidak memaksa Anda untuk menanganinya.

Dalam codelab ini, kita telah membahas dasar-dasar tentang cara menulis kode Kotlin yang juga mendukung penulisan kode Java idiomatis.

Kita telah membahas cara menggunakan anotasi untuk mengubah cara Kotlin menghasilkan bytecode JVM, seperti:

  • @JvmStatic untuk menghasilkan anggota dan metode statis.
  • @JvmOverloads untuk menghasilkan metode yang kelebihan muatan 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
}