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:

kotlin T nullable menyebabkan runtime NPE
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:

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

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

Pada 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 memanggil hashCode().
  • Sinkronisasi lewat synchronized() — synchronized(lock: T & Any, block) butuh lock non-nullable.
  • Cache key — key yang dipakai sebagai identifier harus non-nullable.
  • Pembungkus generik (wrapper) — class yang membungkus T tetapi butuh akses non-nullable di method tertentu.

Aturan Penting #

Beberapa aturan sintaks yang harus diingat:

  1. Hanya Any yang didukung — kita tidak bisa menulis T & String atau T & Number. Hanya T & Any (dan T & Any? yang notabene setara T).
  2. Tidak mengubah tipe T di luar scope — tipe parameter tetap T (yang bisa nullable) untuk pemanggil fungsi.
  3. Smart cast otomatis — setelah T & Any, kompilator tahu x non-nullable sehingga x.toString(), x.hashCode(), dst. aman dipanggil.
  4. Kompatibel dengan batasan tipe — bisa dipakai bersama <T : CharSequence?>.

Perbandingan dengan Alternatif #

Tabel berikut membandingkan T & Any dengan alternatif lain:

PendekatanSintaksKelebihanKekurangan
Cek manualif (x != null) x.hashCode() else nullkompatibel semua versiverbose
x!!x!!.hashCode()ringkasruntime NPE bila null
T & Anyfun foo(x: T & Any)jaminan compile-timebutuh Kotlin 1.8+
Constraint <T : Any>fun <T : Any> foo(x: T)jaminan dari deklarasimengubah 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 #

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.