Kotlin tidak memiliki keyword static — semua method dan field berada di dalam class atau top-level. Untuk memberikan perilaku statis (method/field yang dapat diakses tanpa instance), Kotlin memakai companion object atau object singleton. Kode Java yang memanggil Kotlin melihat Companion.INSTANCE.method() secara default. Anotasi @JvmStatic menghasilkan static method sejati yang dapat dipanggil langsung tanpa perantara. Bab ini menjelaskan translasi tersebut beserta dampaknya pada akses Java.

Tiga Konstruk Statis di Kotlin #

Konstruk KotlinDefault JavaAnotasi untuk Static Sejati
companion objectClassName.Companion.method()@JvmStatic pada method
Top-level objectObjectName.INSTANCE.method()@JvmStatic pada method
Top-level val/varFileNameKt.getX()@JvmField atau const val
Property di companionClassName.Companion.getX()@JvmField untuk field langsung

Default ini berfungsi penuh, namun sintaks Java yang dihasilkan tidak idiomatik. @JvmStatic dan @JvmField menghasilkan bytecode yang lebih bersih dan lebih konsisten dengan idiom Java.

Contoh Lengkap: Companion, Object, @JvmStatic, @JvmField #

Kode berikut mendefinisikan empat konfigurasi statis berbeda dan menunjukkan padanan Java yang dihasilkan:

kotlin
// Demonstrasi static interop: companion object, @JvmStatic, @JvmField, object singleton
//
// - companion object Kotlin -> ClassName.Companion di Java
// - @JvmStatic -> static method sejati tanpa perantara Companion
// - @JvmField -> field public static final, tanpa getter
// - object -> singleton INSTANCE di Java

// 1. Companion object TANPA @JvmStatic
class VersiApp {
    companion object {
        val VERSI: String = "1.0.0"
        fun namaLengkap(): String = "BelajarKotlin $VERSI"
    }
}

// 2. Companion object DENGAN @JvmStatic dan @JvmField
class Konstanta {
    companion object {
        @JvmField
        val PI: Double = 3.14159

        @JvmField
        val MAKSIMAL: Int = 100

        @JvmStatic
        fun namaKelas(): String = Konstanta::class.simpleName ?: "Konstanta"

        @JvmStatic
        fun kuadrat(x: Int): Int = x * x
    }
}

// 3. Top-level object singleton: di Java menjadi Util.INSTANCE
object Pemetaan {
    @JvmStatic
    fun balikString(s: String): String = s.reversed()

    @JvmField
    val DEFAULT_PREFIX: String = "log_"
}

// 4. Data class dengan companion object factory
data class Produk(val id: Long, val nama: String) {
    companion object {
        @JvmStatic
        fun kosong(): Produk = Produk(0, "")

        @JvmStatic
        fun dariBaris(baris: String): Produk {
            val bagian = baris.split(",", limit = 2)
            return if (bagian.size == 2) {
                Produk(bagian[0].toLong(), bagian[1])
            } else {
                kosong()
            }
        }
    }
}

fun main() {
    // 1. Companion object tanpa @JvmStatic
    println("=== Companion tanpa @JvmStatic ===")
    println("VersiApp.VERSI        : ${VersiApp.VERSI}")
    println("VersiApp.namaLengkap() : ${VersiApp.namaLengkap()}")
    // Dari Java:
    //   VersiApp.Companion.getVERSI()
    //   VersiApp.Companion.namaLengkap()

    // 2. Companion object dengan @JvmStatic dan @JvmField
    println()
    println("=== Companion dengan @JvmStatic/@JvmField ===")
    println("Konstanta.PI          : ${Konstanta.PI}")
    println("Konstanta.MAKSIMAL     : ${Konstanta.MAKSIMAL}")
    println("Konstanta.namaKelas() : ${Konstanta.namaKelas()}")
    println("Konstanta.kuadrat(5)  : ${Konstanta.kuadrat(5)}")
    // Dari Java (static sejati):
    //   Konstanta.PI
    //   Konstanta.MAKSIMAL
    //   Konstanta.namaKelas()
    //   Konstanta.kuadrat(5)

    // 3. Object singleton dengan @JvmStatic
    println()
    println("=== Object singleton ===")
    println("Pemetaan.balikString(\"Halo\") : ${Pemetaan.balikString("Halo")}")
    println("Pemetaan.DEFAULT_PREFIX    : ${Pemetaan.DEFAULT_PREFIX}")
    // Dari Java:
    //   Pemetaan.balikString("Halo")   <-- static, tanpa INSTANCE
    //   Pemetaan.DEFAULT_PREFIX         <-- static field, tanpa INSTANCE

    // 4. Factory method di data class
    println()
    println("=== Factory method ===")
    val p1 = Produk.kosong()
    val p2 = Produk.dariBaris("42,Buku Kotlin")
    println("Produk.kosong()           : $p1")
    println("Produk.dariBaris(...)      : $p2")
    // Dari Java:
    //   Produk.kosong()
    //   Produk.dariBaris("42,Buku Kotlin")

    // 5. Perbandingan bytecode signature
    println()
    println("=== Perbandingan signature ===")
    println("// VersiApp.namaLengkap()   ->  VersiApp.Companion.namaLengkap()")
    println("// Konstanta.kuadrat(5)       ->  Konstanta.kuadrat(5)  [static]")
    println("// Pemetaan.balikString(s)   ->  Pemetaan.balikString(s)  [static]")
    println("// Konstanta.PI              ->  Konstanta.PI  [static field]")
    println("// VersiApp.VERSI             ->  VersiApp.Companion.getVERSI()")
}

Output yang Dihasilkan:

text
=== Companion tanpa @JvmStatic ===
VersiApp.VERSI        : 1.0.0
VersiApp.namaLengkap() : BelajarKotlin 1.0.0

=== Companion dengan @JvmStatic/@JvmField ===
Konstanta.PI          : 3.14159
Konstanta.MAKSIMAL     : 100
Konstanta.namaKelas() : Konstanta
Konstanta.kuadrat(5)  : 25

=== Object singleton ===
Pemetaan.balikString("Halo") : olaH
Pemetaan.DEFAULT_PREFIX    : log_

=== Factory method ===
Produk.kosong()           : Produk(id=0, nama=)
Produk.dariBaris(...)      : Produk(id=42, nama=Buku Kotlin)

=== Perbandingan signature ===
// VersiApp.namaLengkap()   ->  VersiApp.Companion.namaLengkap()
// Konstanta.kuadrat(5)       ->  Konstanta.kuadrat(5)  [static]
// Pemetaan.balikString(s)   ->  Pemetaan.balikString(s)  [static]
// Konstanta.PI              ->  Konstanta.PI  [static field]
// VersiApp.VERSI             ->  VersiApp.Companion.getVERSI()

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-13 (companion object tanpa @JvmStatic): di bytecode Java, VersiApp punya inner class Companion dengan instance INSTANCE. Method namaLengkap() dipanggil sebagai VersiApp.Companion.INSTANCE.namaLengkap(). Properti VERSI diakses via VersiApp.Companion.INSTANCE.getVERSI(). Sintaks Java yang dihasilkan berantakan.
  2. Baris 16-29 (companion object dengan @JvmStatic dan @JvmField): @JvmStatic pada method namaKelas() dan kuadrat(x) menghasilkan static method sejati di bytecode Java. @JvmField pada properti PI dan MAKSIMAL menghasilkan field public static final langsung tanpa getter. Sisi Java memanggil Konstanta.kuadrat(5) dan membaca Konstanta.PI secara langsung.
  3. Baris 32-37 (object Pemetaan): object di Kotlin menjadi singleton class dengan instance Pemetaan.INSTANCE. Tanpa @JvmStatic, Java harus menulis Pemetaan.INSTANCE.balikString("Halo"). Dengan @JvmStatic, kompilator menghasilkan static method tambahan Pemetaan.balikString(String) yang mendelegasikan ke INSTANCE. @JvmField val DEFAULT_PREFIX menjadi field public static final.
  4. Baris 40-52 (data class Produk dengan companion factory): pola factory method di companion object sangat umum. Tanpa @JvmStatic, Java harus menulis Produk.Companion.INSTANCE.kosong() — sangat berantakan. Dengan @JvmStatic, Java memanggil Produk.kosong() dan Produk.dariBaris("42,Buku Kotlin") secara langsung, sama seperti idiom Java native.
  5. Baris 67-71 (perbandingan signature): tabel ringkasan ini menunjukkan translasi yang terjadi di bytecode. Perbedaan paling mencolok adalah pada VersiApp (memerlukan Companion. perantara) versus Konstanta (static sejati).

Pemilihan Anotasi: Kapan Pakai Apa #

SkenarioAnotasiAlasan
Library Kotlin murni untuk Kotlintidak perlu anotasiSintaks Kotlin sudah idiomatik
Library Kotlin dipanggil Java@JvmStatic + @JvmFieldJava tidak perlu Companion. perantara
Konstanta string/primitif publikconst valField public static final langsung, tanpa getter
Singleton dengan stateobject saja (cukup)Java pakai INSTANCE eksplisit
Method factory di data class@JvmStaticJava panggil Class.factory() langsung

Referensi Lanjutan #

Selanjutnya #

Bab berikutnya membedah @JvmField dan @JvmStatic secara mendalam — perbedaan keduanya, kapan harus memilih satu dibanding yang lain, dan contoh kasus nyata di library open source.