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 #

#PraktikTujuanAPI Kotlin
1Anotasi nullability di JavaKotlin tahu nullability type Java@Nullable, @NotNull (JetBrains/JSR-305)
2@JvmStatic di companionMethod static Java, bukan instancecompanion object { @JvmStatic fun foo() }
3@JvmOverloadsHasilkan overload untuk default arg@JvmOverloads fun foo(a: Int, b: Int = 0)
4@JvmFieldField publik tanpa getter@JvmField val MAX = 100
5@file:JvmName(...)Nama class top-level bersih@file:JvmName("StringUtils")
6Tipe eksplisit untuk platform typeHindari NPE tidak terdugaval s: String? = javaObj.getName()
7@Throws untuk checked exceptionJava caller wajib catch@Throws(IOException::class) fun read()

Program Demonstrasi Lengkap #

Mari kita gabungkan tujuh praktik di atas dalam satu program yang bisa dijalankan:

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

text
=== 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 Java

Mari kita bedah tujuh praktik tersebut:

  1. @JvmField val TARIF_DEFAULT = 0.11: tanpa getter, jadi field publik yang bisa diakses langsung dari Java sebagai HitungPajak.TARIF_DEFAULT. Cocok untuk konstanta.
  2. @JvmStatic fun hitung(...): di companion object, menghasilkan method static Java. Tanpa anotasi ini, Java harus memanggil HitungPajak.Companion.hitung(...).
  3. @JvmOverloads fun formatMataUang(...): compiler menghasilkan tiga overload: formatMataUang(jumlah), formatMataUang(jumlah, mataUang), formatMataUang(jumlah, mataUang, desimal). Java caller bisa pilih yang sesuai.
  4. val nilai: String? = peta[kunci]: deklarasi eksplisit nullable. HashMap.get() mengembalikan platform type String! — Kotlin tidak tahu nullability sebenarnya. Dengan String?, kita dipaksa memakai ?: atau ?..
  5. @Throws(NumberFormatException::class): menandai method sebagai melempar exception. Java caller wajib catch atau declare. Tanpa ini, IDE Java tidak menampilkan warning.
  6. snapshot(): List<String> = items.toList(): mengembalikan salinan read-only. Java caller bisa memodifikasi salinan tanpa memengaruhi state internal Keranjang. Penting untuk encapsulation.
  7. @JvmField val status: String = "aktif" di data class: field publik langsung, bukan properti dengan getter. Java bisa akses c1.status tanpa getStatus(). 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 @Throws untuk setiap method yang melempar exception checked.
  • Gunakan @file:JvmName("NamaBersih") untuk file top-level functions agar Java caller tidak melihat UtilKt.
  • Hindari ekspose MutableList, MutableMap langsung — 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.List ke kotlin.collections.List lewat tipe Kotlin — runtime tetap instance yang sama.
  • Test edge case null karena Java tidak menjamin non-null.

Referensi Lanjutan #

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.