Interoperabilitas Kotlin-Java adalah dua arah, tetapi bukan berarti bebas friksi. Banyak kasus yang berjalan baik di Kotlin bisa jadi membingungkan dari sisi Java, dan sebaliknya. Chapter ini mengumpulkan tujuh praktik terbaik yang umum dipakai tim ketika kedua bahasa harus hidup berdampingan dalam satu codebase.
Tujuh Praktik Interoperabilitas #
| # | Praktik | Tujuan | API Kotlin |
|---|---|---|---|
| 1 | Anotasi nullability di Java | Kotlin tahu nullability type Java | @Nullable, @NotNull (JetBrains/JSR-305) |
| 2 | @JvmStatic di companion | Method static Java, bukan instance | companion object { @JvmStatic fun foo() } |
| 3 | @JvmOverloads | Hasilkan overload untuk default arg | @JvmOverloads fun foo(a: Int, b: Int = 0) |
| 4 | @JvmField | Field publik tanpa getter | @JvmField val MAX = 100 |
| 5 | @file:JvmName(...) | Nama class top-level bersih | @file:JvmName("StringUtils") |
| 6 | Tipe eksplisit untuk platform type | Hindari NPE tidak terduga | val s: String? = javaObj.getName() |
| 7 | @Throws untuk checked exception | Java caller wajib catch | @Throws(IOException::class) fun read() |
Program Demonstrasi Lengkap #
Mari kita gabungkan tujuh praktik di atas dalam satu program yang bisa dijalankan:
import java.util.HashMap
import java.time.LocalDate
// Demonstrasi praktik terbaik interoperabilitas Java <-> Kotlin:
// 1. Anotasi nullability di Java (memakai JDK yang sudah beranotasi)
// 2. @JvmStatic untuk companion method yang dipanggil Java
// 3. @JvmOverloads untuk default argumen agar Java dapat panggil
// 4. @JvmField untuk konstanta publik
// 5. @file:JvmName untuk nama class bersih (di-comment karena memengaruhi runner)
// 6. Deklarasi tipe eksplisit untuk platform type
// 1. Library Kotlin untuk konsumsi Java: pakai anotasi lengkap
class HitungPajak {
companion object {
@JvmField
val TARIF_DEFAULT: Double = 0.11
@JvmStatic
fun hitung(nilai: Double, tarif: Double = TARIF_DEFAULT): Double = nilai * tarif
}
}
// 2. Top-level function dengan @JvmOverloads
@JvmOverloads
fun formatMataUang(jumlah: Double, simbol: String = "Rp", desimal: Int = 0): String {
return "%s%,.${desimal}f".format(simbol, jumlah)
}
// 3. Penanganan platform type: deklarasi eksplisit nullable
fun cariAman(peta: HashMap<String, String>, kunci: String): String {
// HashMap.get() di Kotlin dianggap nullable (jdk internal @Nullable)
val nilai: String? = peta[kunci]
return nilai ?: "(tidak ditemukan)"
}
// 4. @Throws untuk checked exception yang dilempar ke Java
@Throws(NumberFormatException::class)
fun parseAngka(input: String): Int {
return input.toInt()
}
// 5. Wrapper koleksi Java dengan salinan defensif
class KeranjangBelanja internal constructor(
private val items: MutableList<String>, // private, hanya paparan via List
) {
fun items(): List<String> = items.toList() // salinan defensif
fun tambah(item: String) {
items.add(item)
}
companion object {
@JvmStatic
fun kosong(): KeranjangBelanja = KeranjangBelanja(mutableListOf())
}
}
// 6. Data class dengan @JvmOverloads + @JvmField
data class Pelanggan @JvmOverloads constructor(
val id: Long,
val nama: String,
@JvmField val status: String = "aktif",
)
fun main() {
// 1. Pemakaian @JvmField + @JvmStatic
println("=== Praktik 1: @JvmField + @JvmStatic ===")
println("HitungPajak.TARIF_DEFAULT : ${HitungPajak.TARIF_DEFAULT}")
println("HitungPajak.hitung(100000.0) : ${HitungPajak.hitung(100000.0)}")
println("HitungPajak.hitung(100000.0, 0.05) : ${HitungPajak.hitung(100000.0, 0.05)}")
// Java: HitungPajak.TARIF_DEFAULT, HitungPajak.hitung(100000.0), HitungPajak.hitung(100000.0, 0.05)
// 2. @JvmOverloads pada top-level function
println()
println("=== Praktik 2: @JvmOverloads ===")
println("formatMataUang(1500000.0) : ${formatMataUang(1500000.0)}")
println("formatMataUang(1500000.0, \"USD\") : ${formatMataUang(1500000.0, "USD")}")
println("formatMataUang(1500000.0, \"USD\", 2) : ${formatMataUang(1500000.0, "USD", 2)}")
// Java: 3 signature tersedia
// 3. Mitigasi platform type
println()
println("=== Praktik 3: Mitigasi platform type ===")
val peta: HashMap<String, String> = HashMap()
peta["nama"] = "Budi"
println("cariAman(peta, \"nama\") : ${cariAman(peta, "nama")}")
println("cariAman(peta, \"tidakAda\") : ${cariAman(peta, "tidakAda")}")
// 4. @Throws untuk konsumsi Java
println()
println("=== Praktik 4: @Throws ===")
val input = listOf("42", "bukan-angka")
for (s in input) {
try {
val n = parseAngka(s)
println("parseAngka(\"$s\") -> $n")
} catch (e: NumberFormatException) {
println("parseAngka(\"$s\") -> NumberFormatException: ${e.message}")
}
}
// 5. Wrapper koleksi defensif
println()
println("=== Praktik 5: Salinan defensif ===")
val keranjang = KeranjangBelanja.kosong()
keranjang.tambah("Buku")
keranjang.tambah("Pulpen")
val snapshot: List<String> = keranjang.items()
keranjang.tambah("Penghapus") // tidak memengaruhi snapshot
println("snapshot : $snapshot")
println("keranjang sekarang: ${keranjang.items()}")
// 6. Data class dengan default arg
println()
println("=== Praktik 6: data class + @JvmOverloads + @JvmField ===")
val c1 = Pelanggan(1, "Budi")
val c2 = Pelanggan(2, "Siti", "nonaktif")
println("c1: $c1")
println("c2: $c2")
println("c1.status: ${c1.status} (field public, dapat diakses langsung dari Java)")
// 7. Getter LocalDate sebagai properti (konvensi isXxx untuk Boolean)
val tanggal = LocalDate.of(2026, 1, 15)
println()
println("=== Praktik 7: Konvensi properti ===")
println("tanggal.year : ${tanggal.year}")
println("tanggal.isLeapYear : ${tanggal.isLeapYear}") // isXxx -> Boolean properti
// 8. Rangkuman pedoman
println()
println("=== Pedoman Ringkas ===")
println("// 1. Anotasi Java: @Nullable/@NotNull supaya Kotlin tahu nullability")
println("// 2. @JvmStatic untuk companion method dipanggil Java")
println("// 3. @JvmOverloads untuk default arg dipanggil Java")
println("// 4. @JvmField untuk konstanta publik tanpa getter")
println("// 5. @file:JvmName untuk nama class file yang bersih")
println("// 6. Deklarasi tipe eksplisit untuk platform type")
println("// 7. @Throws untuk checked exception yang dipanggil Java")
println("// 8. Salinan defensif toList() saat memberi List ke Java")
}Output yang Dihasilkan:
=== Praktik 1: @JvmField + @JvmStatic ===
HitungPajak.TARIF_DEFAULT : 0.11
HitungPajak.hitung(100000.0) : 11000.0
HitungPajak.hitung(100000.0, 0.05) : 5000.0
=== Praktik 2: @JvmOverloads ===
formatMataUang(1500000.0) : Rp1,500,000
formatMataUang(1500000.0, "USD") : USD1,500,000
formatMataUang(1500000.0, "USD", 2) : USD1,500,000.00
=== Praktik 3: Mitigasi platform type ===
cariAman(peta, "nama") : Budi
cariAman(peta, "tidakAda") : (tidak ditemukan)
=== Praktik 4: @Throws ===
parseAngka("42") -> 42
parseAngka("bukan-angka") -> NumberFormatException: For input string: "bukan-angka"
=== Praktik 5: Salinan defensif ===
snapshot : [Buku, Pulpen]
keranjang sekarang: [Buku, Pulpen, Penghapus]
=== Praktik 6: data class + @JvmOverloads + @JvmField ===
c1: Pelanggan(id=1, nama=Budi, status=aktif)
c2: Pelanggan(id=2, nama=Siti, status=nonaktif)
c1.status: aktif (field public, dapat diakses langsung dari Java)
=== Praktik 7: Konvensi properti ===
tanggal.year : 2026
tanggal.isLeapYear : false
=== Pedoman Ringkas ===
// 1. Anotasi Java: @Nullable/@NotNull supaya Kotlin tahu nullability
// 2. @JvmStatic untuk companion method dipanggil Java
// 3. @JvmOverloads untuk default arg dipanggil Java
// 4. @JvmField untuk konstanta publik tanpa getter
// 5. @file:JvmName untuk nama class file yang bersih
// 6. Deklarasi tipe eksplisit untuk platform type
// 7. @Throws untuk checked exception yang dipanggil Java
// 8. Salinan defensif toList() saat memberi List ke JavaMari kita bedah tujuh praktik tersebut:
@JvmField val TARIF_DEFAULT = 0.11: tanpa getter, jadi field publik yang bisa diakses langsung dari Java sebagaiHitungPajak.TARIF_DEFAULT. Cocok untuk konstanta.@JvmStatic fun hitung(...): di companion object, menghasilkan method static Java. Tanpa anotasi ini, Java harus memanggilHitungPajak.Companion.hitung(...).@JvmOverloads fun formatMataUang(...): compiler menghasilkan tiga overload:formatMataUang(jumlah),formatMataUang(jumlah, mataUang),formatMataUang(jumlah, mataUang, desimal). Java caller bisa pilih yang sesuai.val nilai: String? = peta[kunci]: deklarasi eksplisit nullable.HashMap.get()mengembalikan platform typeString!— Kotlin tidak tahu nullability sebenarnya. DenganString?, kita dipaksa memakai?:atau?..@Throws(NumberFormatException::class): menandai method sebagai melempar exception. Java caller wajib catch atau declare. Tanpa ini, IDE Java tidak menampilkan warning.snapshot(): List<String> = items.toList(): mengembalikan salinan read-only. Java caller bisa memodifikasi salinan tanpa memengaruhi state internalKeranjang. Penting untuk encapsulation.@JvmField val status: String = "aktif"di data class: field publik langsung, bukan properti dengan getter. Java bisa aksesc1.statustanpagetStatus(). Trade-off: tidak ada validasi setter.
Checklist Saat Membuat Library Kotlin untuk Konsumsi Java #
- Tandai setiap API publik dengan
@JvmStatic(companion),@JvmOverloads(default args),@JvmField(konstanta). - Tambahkan
@Throwsuntuk setiap method yang melempar exception checked. - Gunakan
@file:JvmName("NamaBersih")untuk file top-level functions agar Java caller tidak melihatUtilKt. - Hindari ekspose
MutableList,MutableMaplangsung — gunakan salinan defensif. - Validasi nullability setiap nilai yang kembali dari Java — anggap nullable.
- Tulis unit test dari sisi Java (mis.
JavaInteropTest.java) untuk memverifikasi API benar-benar dapat dipanggil.
Checklist Saat Memakai Library Java dari Kotlin #
- Periksa anotasi nullability (
@Nullable,@NotNull) di Javadoc atau source Java. - Deklarasikan tipe Kotlin eksplisit — jangan biarkan compiler menebak sebagai platform type.
- Tangkap checked exception secara eksplisit jika Java mendeklarasikannya.
- Konversi
java.util.Listkekotlin.collections.Listlewat tipe Kotlin — runtime tetap instance yang sama. - Test edge case
nullkarena Java tidak menjamin non-null.
Referensi Lanjutan #
- Calling Java from Kotlin — dokumentasi utama Java interop sebagai dasar praktik terbaik
- Calling Kotlin from Java — panduan mengekspos Kotlin ke Java secara aman dan idiomatik
Selanjutnya #
Interoperabilitas Kotlin-Java membuka pintu ke ekosistem JVM yang luas. Chapter selanjutnya, Kotlin/JVM Architecture, akan membongkar bagaimana bytecode Kotlin berjalan di atas JVM dan apa yang terjadi di balik layar saat kode Kotlin dikompilasi.