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 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:

Download Zip

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
}