Tipe parameter generik T di Kotlin secara default bisa nullable — bila kita memanggil fungsi fun <T> foo(x: T) dengan T = String?, kompilator tetap memperbolehkan pemanggilan. Hal ini menjadi masalah saat implementasi internal butuh jaminan x non-nullable, mis. untuk memanggil x.hashCode() atau x.length. Definitely non-nullable types dengan sintaks T & Any memecahkan masalah ini: kita bisa memaksa T menjadi non-nullable di scope tertentu tanpa mengubah batasan tipe parameter. Pada chapter ini kita akan memahami masalah, melihat sintaks resmi, dan menulis contoh konkret yang memanfaatkan T & Any.
Masalah Tipe Parameter Nullable #
Perhatikan cuplikan berikut:
class Penampung<T>(val isi: T) {
fun deskripsiNaive(): String = "isi=${isi.toString()}"
}
val a = Penampung<String?>(null) // T = String?, isi = null
println(a.deskripsiNaive()) // Aman: toString() menerima null
fun <T> cetakHashCode(x: T): Int? {
return if (x == null) null else x.hashCode()
}Di kelas Penampung<String?>, isi bisa null. Pemanggilan isi.hashCode() akan menyebabkan NullPointerException saat runtime. Sebelum T & Any, kita harus menulis x!! atau melakukan if (x != null) manual di setiap pemanggilan.
Sintaks T & Any #
Sintaks T & Any disebut intersection type. Kompilator memperlakukan T sebagai T (yang bisa nullable) di luar scope, tetapi sebagai T & Any (non-nullable) di dalam scope tertentu:
// definitely_non_nullable_types.kt
// Mendemonstrasikan tipe parameter non-nullable dengan sintaks `T & Any`.
// Fitur stabil sejak Kotlin 1.8 — memungkinkan kita memaksakan
// non-nullable untuk parameter tipe `T` dalam scope tertentu.
// 1. Masalah: T bisa nullable, method yang butuh non-nullable tidak aman dipanggil
class Penampung<T>(val isi: T) {
// toString() aman untuk null, jadi method ini bisa dipanggil walau T = String?
fun deskripsiNaive(): String = "isi=${isi.toString()}"
}
// 2. Solusi: T & Any — paksa T menjadi non-nullable di scope method
class PenampungAman<T>(val isi: T) {
// Bila T = String?, `isi` masih nullable di sini.
// Di method prosesNonNullable, kita terima `T & Any` (intersection type).
// Gunakan intersection type `T & Any` sebagai parameter
fun prosesNonNullable(input: T & Any): String = input.toString().uppercase()
}
// 3. Fungsi generik dengan T nullable, tetapi internal memaksa non-nullable
fun <T> panjang(x: T): Int? {
// x bisa null, jadi panjang() mengembalikan null
return if (x == null) null else x.toString().length
}
fun <T> panjangAman(x: T & Any): Int = x.toString().length
// 4. Helper type alias untuk intersection type (tipe parameter)
fun <T : CharSequence?> panjangCharSequence(s: T & Any): Int = s.length
fun main() {
println("=== 1. Masalah tipe parameter nullable ===")
val a: Penampung<String?> = Penampung<String?>(null)
val b: Penampung<String> = Penampung("halo")
println("a.isi = ${a.isi} (nullable, tipe T = String?)")
println("b.isi = ${b.isi} (non-nullable, tipe T = String)")
println("a.deskripsiNaive() = ${a.deskripsiNaive()}")
println()
println("=== 2. Paksa non-nullable dengan T & Any ===")
val aman = PenampungAman("halo")
println("prosesNonNullable(\"halo\") = ${aman.prosesNonNullable("halo")}")
println()
println("=== 3. Perbandingan dua versi panjang ===")
println("panjang(null) = ${panjang<String?>(null)}")
println("panjang(\"abc\") = ${panjang<String?>("abc")}")
println("panjangAman(\"abc\") = ${panjangAman("abc")}")
println()
println("=== 4. T & Any dengan batasan tipe dasar ===")
val s1: CharSequence = "halo dunia"
val s2: CharSequence = StringBuilder("builder string")
println("panjangCharSequence(\"halo dunia\") = ${panjangCharSequence(s1)}")
println("panjangCharSequence(StringBuilder) = ${panjangCharSequence(s2)}")
println()
println("=== 5. Ringkasan sintaks ===")
val aturan = listOf(
"T & Any -> T dipaksa non-nullable di scope tersebut",
"T & Any tidak mengubah tipe T di luar scope",
"T & Any sering dipakai untuk toString(), length, equals()",
"Tidak bisa menulis T & String; hanya Any yang didukung"
)
aturan.forEach { println(" - $it") }
}Output yang Dihasilkan:
=== 1. Masalah tipe parameter nullable ===
a.isi = null (nullable, tipe T = String?)
b.isi = halo (non-nullable, tipe T = String)
a.deskripsiNaive() = isi=null
=== 2. Paksa non-nullable dengan T & Any ===
prosesNonNullable("halo") = HALO
=== 3. Perbandingan dua versi panjang ===
panjang(null) = null
panjang("abc") = 3
panjangAman("abc") = 3
=== 4. T & Any dengan batasan tipe dasar ===
panjangCharSequence("halo dunia") = 10
panjangCharSequence(StringBuilder) = 14
=== 5. Ringkasan sintaks ===
- T & Any -> T dipaksa non-nullable di scope tersebut
- T & Any tidak mengubah tipe T di luar scope
- T & Any sering dipakai untuk toString(), length, equals()
- Tidak bisa menulis T & String; hanya Any yang didukungPada cuplikan di atas, prosesNonNullable(input: T & Any) menerima argumen bertipe T tetapi kompilator menjamin input tidak null di dalam fungsi. Hal ini memungkinkan pemanggilan input.toString().uppercase() tanpa risiko NPE. Di panjangAman, kita tidak perlu cek null manual — kompilator sudah menjamin x non-nullable.
Kapan Memakai T & Any #
Sintaks T & Any berguna saat kita punya fungsi generik yang secara default menerima tipe nullable, tetapi implementasi internal membutuhkan non-nullable. Beberapa skenario umum:
- Implementasi
equals()pada kelas generik —equals(other: T & Any)agar aman memanggilhashCode(). - Sinkronisasi lewat
synchronized()—synchronized(lock: T & Any, block)butuhlocknon-nullable. - Cache key — key yang dipakai sebagai identifier harus non-nullable.
- Pembungkus generik (wrapper) — class yang membungkus
Ttetapi butuh akses non-nullable di method tertentu.
Aturan Penting #
Beberapa aturan sintaks yang harus diingat:
- Hanya
Anyyang didukung — kita tidak bisa menulisT & StringatauT & Number. HanyaT & Any(danT & Any?yang notabene setaraT). - Tidak mengubah tipe
Tdi luar scope — tipe parameter tetapT(yang bisa nullable) untuk pemanggil fungsi. - Smart cast otomatis — setelah
T & Any, kompilator tahuxnon-nullable sehinggax.toString(),x.hashCode(), dst. aman dipanggil. - Kompatibel dengan batasan tipe — bisa dipakai bersama
<T : CharSequence?>.
Perbandingan dengan Alternatif #
Tabel berikut membandingkan T & Any dengan alternatif lain:
| Pendekatan | Sintaks | Kelebihan | Kekurangan |
|---|---|---|---|
| Cek manual | if (x != null) x.hashCode() else null | kompatibel semua versi | verbose |
x!! | x!!.hashCode() | ringkas | runtime NPE bila null |
T & Any | fun foo(x: T & Any) | jaminan compile-time | butuh Kotlin 1.8+ |
Constraint <T : Any> | fun <T : Any> foo(x: T) | jaminan dari deklarasi | mengubah signature pemanggil |
Pilihan terbaik bergantung pada konteks: bila T boleh nullable untuk pemanggil, gunakan T & Any di method tertentu yang butuh non-nullable. Bila T selalu non-nullable di seluruh API, gunakan constraint <T : Any> di deklarasi kelas.
Referensi Lanjutan #
- What’s new in Kotlin 2.0
— ringkasan Kotlin 2.0 yang menstabilkan tipe
T & Any(definitely non-nullable types) - Kotlin documentation home
— halaman utama dokumentasi Kotlin untuk mempelajari interaksi
T & Anydengan tipe generik nullable
Selanjutnya #
Setelah menguasai T & Any, kita lanjut ke fitur inferensi yang lebih pintar — builder inference — yang membuat buildList { add("...") } bisa menyimpulkan tipe elemen tanpa anotasi eksplisit.