Platform type T! adalah sumber NullPointerException tersekat karena Kotlin tidak tahu apakah method Java dapat mengembalikan null atau tidak. Solusi permanennya adalah menambahkan anotasi nullability di sisi Java — @Nullable, @NotNull, atau JSR-305. Bab ini menjelaskan anotasi yang dibaca Kotlin, cara kerjanya, dan dampaknya pada kode Kotlin yang mengonsumsi method Java beranotasi.
Anotasi yang Dibaca Kotlin #
Kotlin mengenali beberapa jenis anotasi nullability:
| Sumber Anotasi | Paket | Contoh |
|---|---|---|
| JetBrains | org.jetbrains.annotations | @Nullable, @NotNull |
| JSR-305 | javax.annotation | @Nullable, @Nonnull, @ParametersAreNonnullByDefault |
| AndroidX | androidx.annotation | @Nullable, @NonNull |
| Spring Framework | org.springframework.lang | @Nullable, @NonNull |
| FindBugs/SpotBugs | edu.umd.cs.findbugs.annotations | @Nullable, @NonNull, @CheckForNull |
| JDK internal | jdk.internal (via @Nullable di source) | Anotasi pada Map.get, dll. |
Saat kompilator Kotlin melihat salah satu anotasi ini pada method Java, tipe kembalian diproses: @Nullable menjadi T?, @NotNull menjadi T. Tanpa anotasi, tipe tetap menjadi platform type T!.
Cara Kerja: Sebelum vs Sesudah Anotasi #
Tabel berikut membandingkan kode Java tanpa dan dengan anotasi, beserta tipe yang dilihat Kotlin:
public class Repo {
public String cari(String id) { ... }
}| Sisi Kotlin | Tipe | Risiko |
|---|---|---|
repo.cari("x") ditugaskan ke String | platform String! | Bisa null, tidak ada peringatan |
repo.cari("x") ditugaskan ke String? | String? | Aman |
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
public class Repo {
@Nullable
public String cariOpsional(String id) { ... }
@NotNull
public String cariWajib(String id) { ... }
}| Sisi Kotlin | Tipe | Risiko |
|---|---|---|
repo.cariOpsional("x") | String? | Eksplisit nullable, aman |
repo.cariWajib("x") | String | Eksplisit non-null, aman |
Contoh Lengkap: Efek Anotasi JDK Bawaan #
Kode berikut memakai kelas JDK yang sudah mengandung anotasi nullability (modern JDK mulai menambahkan @Nullable pada method seperti Map.get). Efeknya, Kotlin memperlakukan tipe kembalian sebagai String? tanpa perlu deklarasi eksplisit:
import java.util.HashMap
import java.util.ArrayList
// Demonstrasi cara Kotlin membaca anotasi nullability dari JDK.
// JDK modern mengannotasi method seperti Map.get dengan @Nullable
// (melalui jsr-305 yang dibungkus jdk.internal), sehingga Kotlin
// memperlakukan tipe kembalian sebagai nullable.
//
// Anotasi yang dibaca Kotlin:
// - @org.jetbrains.annotations.Nullable -> T?
// - @org.jetbrains.annotations.NotNull -> T
// - @javax.annotation.Nullable (JSR-305) -> T? (bila library tersedia)
// - @javax.annotation.Nonnull (JSR-305) -> T (bila library tersedia)
//
// File ini memakai kelas JDK yang sudah memiliki anotasi built-in
// untuk mendemonstrasikan efeknya dari sisi Kotlin.
fun main() {
// 1. HashMap.get(): JDK mengannotasi @Nullable -> Kotlin String?
val peta: HashMap<String, String> = HashMap()
peta["nama"] = "Budi"
val namaAman: String? = peta["nama"]
val namaTidakAda: String? = peta["tidakAda"]
println("=== HashMap.get(): @Nullable dari JDK ===")
println("namaAman : $namaAman (tipe: ${if (namaAman == null) "null" else "non-null"})")
println("namaTidakAda : $namaTidakAda (tipe: ${if (namaTidakAda == null) "null" else "non-null"})")
// 2. HashMap.put(): mengembalikan nilai LAMA yang asosiasi dengan kunci,
// atau null bila tidak ada. Anotasi @Nullable -> V?
val lama: String? = peta.put("nama", "Siti")
println()
println("=== HashMap.put(): nilai lama ===")
println("nilai lama untuk 'nama': $lama")
val lama2: String? = peta.put("baru", "NilaiBaru")
println("nilai lama untuk 'baru': $lama2 (null = tidak ada)")
// 3. ArrayList.remove(int): mengembalikan elemen yang dihapus (non-null bila E non-null)
val daftar: ArrayList<String> = ArrayList()
daftar.add("Pertama")
daftar.add("Kedua")
val dihapus: String = daftar.removeAt(0) // tidak nullable karena E = String non-null
println()
println("=== ArrayList.removeAt(): non-null ===")
println("dihapus : $dihapus")
println("daftar sekarang: $daftar")
// 4. Map.getOrDefault: anotasi @Nullable pada parameter default, @NotNull pada return
val nilaiDefault: String = peta.getOrDefault("tidakAda", "default")
println()
println("=== getOrDefault: non-null ===")
println("nilaiDefault : $nilaiDefault")
// 5. Contoh perbandingan: method Java tanpa anotasi jadi platform type
println()
println("=== Method tanpa anotasi: platform type ===")
val berkas = java.io.File("/tmp/nullability_demo.txt")
berkas.writeText("hello")
val path: String? = berkas.absolutePath // File.getAbsolutePath() tidak @Nullable, jadi String!
val pathNonNull: String = berkas.absolutePath // aman karena platform type, bisa non-null
println("absolutePath : $path")
println("nonNullPath : $pathNonNull")
berkas.delete()
}Output yang Dihasilkan:
=== HashMap.get(): @Nullable dari JDK ===
namaAman : Budi (tipe: non-null)
namaTidakAda : null (tipe: null)
=== HashMap.put(): nilai lama ===
nilai lama untuk 'nama': Budi
nilai lama untuk 'baru': null (null = tidak ada)
=== ArrayList.removeAt(): non-null ===
dihapus : Pertama
daftar sekarang: [Kedua]
=== getOrDefault: non-null ===
nilaiDefault : default
=== Method tanpa anotasi: platform type ===
absolutePath : /tmp/nullability_demo.txt
nonNullPath : /tmp/nullability_demo.txtMari kita telaah baris-baris kode di atas:
- Baris 23 (
val namaAman: String? = peta["nama"]):HashMap.get(K)dianotasi@Nullabledi JDK modern. Kotlin membaca anotasi tersebut dan memperlakukan tipe kembalian sebagaiV?(yaituString?). Tanpa deklarasiString?eksplisit, kompilator tetap menerima nilai sebagai platform type — namun anotasi memberi sinyal kuat yang dipakai Kotlin saat inferensi tipe. - Baris 33 (
val lama: String? = peta.put("nama", "Siti")):HashMap.putjuga@Nullablekarena mengembalikan nilai lama yang mungkinnull(bila kunci baru). Kotlin memperlakukan hasil sebagaiString?tanpa ambiguitas. - Baris 41 (
val dihapus: String = daftar.removeAt(0)):ArrayList.remove(int)dianotasi@NotNulluntuk elemen generik non-null. KarenaArrayList<String>(bukanArrayList<String?>), Kotlin memperlakukan hasil sebagaiStringnon-nullable. Penugasan ke variabelStringlangsung berhasil. - Baris 48 (
val nilaiDefault: String = peta.getOrDefault(...)):getOrDefaultmengembalikan nilai default non-null bila disediakan. JDK mengannotasi tipe kembalian sebagai@NotNullkarena kontraknya jelas: salah satu dari nilai aktual atau default, keduanya non-null. - Baris 55-56 (File.getAbsolutePath): method ini tidak dianotasi
@Nullablemaupun@NotNulldi JDK. Hasilnya adalah platform typeString!. Kita bisa menugaskan keString?maupunString— kompilator menerima keduanya, namun bila method Java ternyata mengembalikannull, kode Kotlin akan melempar NPE saat runtime.
Anotasi Level Paket (JSR-305) #
JSR-305 menyediakan anotasi @ParametersAreNonnullByDefault yang dapat dipasang pada level paket Java, sehingga semua parameter di paket tersebut dianggap @Nonnull kecuali diberi anotasi @Nullable eksplisit. Kotlin membaca anotasi ini dan menerapkan nullability default ke seluruh paket.
@ParametersAreNonnullByDefault
package com.example.repo;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.Nullable;
public class Repo {
public String cari(String id) { ... } // NonNull
public @Nullable String cariOpsional(String id) { ... } // Nullable
}Referensi Lanjutan #
- Calling Java from Kotlin — dokumentasi Java interop Kotlin termasuk konversi nullability-annotations
Selanjutnya #
Nullability adalah satu aspek dari translasi tipe antar Java dan Kotlin. Aspek lain yang sama pentingnya adalah translasi getter/setter Java menjadi properti Kotlin. Bab Java Properties dan Kotlin Properties akan membahas aturan sintaks properti yang dihasilkan kompilator Kotlin dari method getXxx/setXxx Java.