Bab sebelumnya memperkenalkan @JvmStatic dan @JvmField sebagai anotasi untuk menghasilkan static sejati. Bab ini membedah keduanya secara mendalam: kapan harus memilih satu dibanding yang lain, perbedaan bytecode yang dihasilkan, dan jebakan umum yang muncul saat pemakaian. Pemahaman mendalam terhadap kedua anotasi ini penting karena kesalahan pemilihan menghasilkan API Java yang verbose atau bahkan tidak idiomatik.

Perbedaan Inti @JvmField vs @JvmStatic #

AnotasiBerlaku PadaEfek Bytecode
@JvmFieldProperty val/varField public (static bila di companion/object), tanpa getter/setter
@JvmStaticMethod di companion/objectStatic method sejati yang didelegasikan ke Companion.INSTANCE
const valTop-level / companion val (primitif/String)Field public static final inline

Perbedaan kunci: @JvmField menghapus getter/setter, @JvmStatic menambah static method delegasi. Keduanya saling melengkapi — bisa dipakai bersamaan pada satu companion object.

Lima Konfigurasi yang Umum #

Kode berikut mendefinisikan lima variasi properti/method di companion object, lalu menampilkan padanan Java yang dihasilkan untuk masing-masing:

kotlin
// Demonstrasi @JvmField vs @JvmStatic: perbedaan, kapan dipakai, dan efek bytecode
//
// @JvmField: menghasilkan field public (static bila di companion/object), tanpa getter/setter
// @JvmStatic: menghasilkan static method sejati di bytecode Java
//
// Perbandingan konkrit:
// - val biasa        -> getX() dipanggil
// - @JvmField val     -> field X dibaca langsung, tanpa getX()
// - fun biasa di comp -> Companion.fun() dipanggil
// - @JvmStatic fun     -> Class.fun() dipanggil

class MatematikaDasar {
    companion object {
        // A: val biasa -> Java: MatematikaDasar.Companion.getPI()
        val PI_TANPA_JVMFIELD: Double = 3.14159

        // B: @JvmField val -> Java: MatematikaDasar.PI (field langsung)
        @JvmField
        val PI_DENGAN_JVMFIELD: Double = 3.14159

        // C: const val -> Java: MatematikaDasar.PI (public static final, inline)
        const val NAMA: String = "MatematikaDasar"

        // D: fun biasa -> Java: MatematikaDasar.Companion.kuadratTanpa(5)
        fun kuadratTanpa(x: Int): Int = x * x

        // E: @JvmStatic fun -> Java: MatematikaDasar.kuadratStatic(5)
        @JvmStatic
        fun kuadratStatic(x: Int): Int = x * x
    }
}

// @JvmField pada top-level property -> static field di FileNameKt
@JvmField
val MAKS_PRODUK: Int = 100

// @JvmField pada instance property (non-companion) -> field public biasa di instance
class KonfigurasiApp {
    @JvmField
    val versi: String = "1.0.0"

    @JvmField
    var hitungPengguna: Int = 0
}

// @JvmField pada object singleton -> static field
object PenyusunUrl {
    @JvmField
    val HOST: String = "api.example.com"

    @JvmStatic
    fun bangun(path: String): String = "https://$HOST/$path"
}

fun main() {
    // A vs B: val biasa vs @JvmField val di companion
    println("=== A vs B: companion val ===")
    println("PI_TANPA_JVMFIELD      : ${MatematikaDasar.PI_TANPA_JVMFIELD}")
    println("PI_DENGAN_JVMFIELD     : ${MatematikaDasar.PI_DENGAN_JVMFIELD}")
    println("// Java:")
    println("//   MatematikaDasar.Companion.getPI_TANPA_JVMFIELD()")
    println("//   MatematikaDasar.PI_DENGAN_JVMFIELD (field langsung)")

    // C: const val
    println()
    println("=== C: const val ===")
    println("NAMA                   : ${MatematikaDasar.NAMA}")
    println("// Java: MatematikaDasar.NAMA (public static final inline)")

    // D vs E: fun biasa vs @JvmStatic fun
    println()
    println("=== D vs E: companion fun ===")
    println("kuadratTanpa(5)        : ${MatematikaDasar.kuadratTanpa(5)}")
    println("kuadratStatic(5)       : ${MatematikaDasar.kuadratStatic(5)}")
    println("// Java:")
    println("//   MatematikaDasar.Companion.kuadratTanpa(5)")
    println("//   MatematikaDasar.kuadratStatic(5)  [static method]")

    // @JvmField pada top-level property
    println()
    println("=== @JvmField top-level ===")
    println("MAKS_PRODUK            : $MAKS_PRODUK")
    println("// Java: Jvm_field_dan_jvm_staticKt.MAKS_PRODUK (static field)")

    // @JvmField pada instance property
    println()
    println("=== @JvmField instance ===")
    val app = KonfigurasiApp()
    println("app.versi              : ${app.versi}")
    println("app.hitungPengguna     : ${app.hitungPengguna}")
    app.hitungPengguna = 5
    println("Setelah set: app.hitungPengguna = ${app.hitungPengguna}")
    println("// Java:")
    println("//   app.versi (field public)")
    println("//   app.hitungPengguna = 5 (field public, assignable)")

    // @JvmField + @JvmStatic pada object singleton
    println()
    println("=== Object singleton ===")
    println("PenyusunUrl.HOST       : ${PenyusunUrl.HOST}")
    println("PenyusunUrl.bangun(\"v1/users\") : ${PenyusunUrl.bangun("v1/users")}")
    println("// Java:")
    println("//   PenyusunUrl.HOST (static field)")
    println("//   PenyusunUrl.bangun(\"v1/users\")  [static method]")
}

Output yang Dihasilkan:

text
=== A vs B: companion val ===
PI_TANPA_JVMFIELD      : 3.14159
PI_DENGAN_JVMFIELD     : 3.14159
// Java:
//   MatematikaDasar.Companion.getPI_TANPA_JVMFIELD()
//   MatematikaDasar.PI_DENGAN_JVMFIELD (field langsung)

=== C: const val ===
NAMA                   : MatematikaDasar
// Java: MatematikaDasar.NAMA (public static final inline)

=== D vs E: companion fun ===
kuadratTanpa(5)        : 25
kuadratStatic(5)       : 25
// Java:
//   MatematikaDasar.Companion.kuadratTanpa(5)
//   MatematikaDasar.kuadratStatic(5)  [static method]

=== @JvmField top-level ===
MAKS_PRODUK            : 100
// Java: Jvm_field_dan_jvm_staticKt.MAKS_PRODUK (static field)

=== @JvmField instance ===
app.versi              : 1.0.0
app.hitungPengguna     : 0
Setelah set: app.hitungPengguna = 5
// Java:
//   app.versi (field public)
//   app.hitungPengguna = 5 (field public, assignable)

=== Object singleton ===
PenyusunUrl.HOST       : api.example.com
PenyusunUrl.bangun("v1/users") : https://api.example.com/v1/users
// Java:
//   PenyusunUrl.HOST (static field)
//   PenyusunUrl.bangun("v1/users")  [static method]

Mari kita telaah baris-baris kode di atas:

  1. Baris 14 (val PI_TANPA_JVMFIELD tanpa anotasi): properti val biasa di companion object menghasilkan getter getPI_TANPA_JVMFIELD() di instance Companion. Sisi Java memanggil MatematikaDasar.Companion.getPI_TANPA_JVMFIELD() — verbose. Sisi Kotlin tetap menulis MatematikaDasar.PI_TANPA_JVMFIELD (kompilator menerjemahkan).
  2. Baris 18 (@JvmField val PI_DENGAN_JVMFIELD): anotasi ini menghapus getter, menghasilkan field public static final di bytecode Java. Sisi Java membaca MatematikaDasar.PI_DENGAN_JVMFIELD langsung tanpa perantara Companion atau method getter. Inilah idiom yang dipakai library Java seperti Math.PI.
  3. Baris 22 (const val NAMA): const adalah modifier bawaan Kotlin untuk properti compile-time constant (harus primitif atau String). Hasilnya field public static final yang dapat di-inline oleh kompilator Java di lokasi pemanggilan — lebih efisien daripada @JvmField untuk konstanta literal.
  4. Baris 25-26 (fun kuadratTanpa(x) tanpa anotasi): method di companion object tanpa @JvmStatic menjadi method instance pada Companion. Sisi Java harus menulis MatematikaDasar.Companion.kuadratTanpa(5) — sangat tidak idiomatik.
  5. Baris 29 (@JvmStatic fun kuadratStatic(x)): anotasi menghasilkan static method sejati di class MatematikaDasar. Sisi Java memanggil MatematikaDasar.kuadratStatic(5) langsung. Kompilator tetap mempertahankan method Companion.kuadratStatic untuk konsistensi internal, namun idiom Java memakai static sejati.
  6. Baris 35 (@JvmField val MAKS_PRODUK top-level): anotasi pada top-level property menghasilkan field public static final di class Jvm_field_dan_jvm_staticKt. Sisi Java membaca Jvm_field_dan_jvm_staticKt.MAKS_PRODUK langsung.
  7. Baris 39-44 (KonfigurasiApp dengan @JvmField var): pada instance (non-companion), @JvmField membuat field public langsung. Bila properti var, field menjadi assignable dari luar class. Ini menghapus enkapsulasi — hanya pakai bila kita memang ingin field public mutable (jarang dianjurkan).
  8. Baris 49-55 (object PenyusunUrl): kombinasi @JvmField val HOST + @JvmStatic fun bangun() di object singleton menghasilkan padanan Java yang sangat bersih. Sisi Java memanggil PenyusunUrl.bangun("v1/users") dan membaca PenyusunUrl.HOST tanpa perantara INSTANCE.

Jebakan Umum #

JebakanAkibatSolusi
@JvmField pada var di instanceField public mutable tanpa validasiPakai var biasa + custom setter
@JvmField pada properti dengan custom getterKompilasi gagal — @JvmField melarang accessor customHapus custom getter atau hapus @JvmField
@JvmStatic pada top-level functionTidak ada efek — @JvmStatic hanya untuk companion/objectPakai @file:JvmName("Util") saja
const val pada tipe non-primitifKompilasi gagal — const hanya primitif/StringPakai @JvmField val untuk objek
@JvmStatic pada var propertyTidak ada efek — @JvmStatic hanya untuk methodPakai @JvmField untuk field

Referensi Lanjutan #

Selanjutnya #

Selain @JvmStatic dan @JvmField, ada anotasi @JvmOverloads yang menghasilkan beberapa overload Java untuk default argumen Kotlin. Bab berikutnya membahas cara kerja dan strategi pemakaiannya.