Pada chapter sebelumnya kita melihat bahwa nested class default bersifat statis terhadap outer — tanpa referensi ke instance outer, ia tidak dapat membaca property instance. Kadang kita justru membutuhkan akses tersebut: misalnya ketika sebuah helper harus membaca state outer atau memodifikasi list milik outer. Kotlin menyediakan keyword inner untuk membuat inner class — class bersarang yang secara implisit membawa referensi ke instance outer class. Pada chapter ini kita akan mendeklarasikan inner class, mengakses property outer dari dalam inner, dan memakai sintaks this@Outer untuk merujuk ke instance outer secara eksplisit.

Apa Itu Inner Class #

Inner class adalah class bersarang yang dideklarasikan dengan keyword inner sebelum class. Setiap instance inner class memiliki referensi implisit ke instance outer class yang membuatnya. Karena referensi tersebut, inner class dapat membaca property dan memanggil method instance milik outer seolah-olah anggota tersebut adalah miliknya sendiri.

Untuk membuat instance inner class, kita wajib memiliki instance outer terlebih dahulu. Sintaks pembuatannya adalah outerInstance.Inner() — berbeda dari nested class biasa yang dapat dipakai tanpa instance outer. Batasan ini mencerminkan ketergantungan struktural antara inner dan outer.

Deklarasi dan Pemakaian #

Contoh berikut mendefinisikan class Tumpukan dengan property kapasitas dan penyimpanan. Inner class Item membaca kedua property outer tersebut saat method dorong() dipanggil. Contoh juga menunjukkan inner class yang memodifikasi state outer (PenghitungHuruf menambah counter dihitung) dan akses eksplisit ke instance outer via this@Tumpukan.

kotlin
// inner_classes.kt
// Mendemonstrasikan keyword `inner` — class bersarang yang membawa
// referensi ke instance outer class.

class Tumpukan(private val kapasitas: Int) {
    private val penyimpanan = mutableListOf<String>()

    // Inner class membawa referensi ke instance Tumpukan.
    // Dapat mengakses property `kapasitas` dan `penyimpanan` milik outer.
    inner class Item(private val nilai: String) {
        fun dorong() {
            if (penyimpanan.size >= kapasitas) {
                println("Tumpukan penuh, tidak bisa menambah '$nilai'")
                return
            }
            penyimpanan.add(nilai)
            println("Menambahkan '$nilai' (isi: ${penyimpanan.size}/$kapasitas)")
        }

        fun aksesOuter(): Tumpukan = this@Tumpukan
    }

    fun ukuran(): Int = penyimpanan.size

    fun cetakSemua() {
        println("Isi tumpukan: ${penyimpanan.joinToString(", ")}")
    }
}

class Kata(private val kata: String) {
    private var dihitung = 0

    inner class PenghitungHuruf {
        fun hitung(target: Char): Int {
            dihitung++
            return kata.count { it == target }
        }

        fun berapaKaliDihitung(): Int = dihitung
    }
}

class PetaWarna {
    private val pasangan = mutableListOf<Pair<String, String>>()

    inner class Pencari(private val kunci: String) {
        fun temukan(): String? {
            for ((k, v) in pasangan) {
                if (k == kunci) return v
            }
            return null
        }

        fun tambahkan(nilai: String) {
            pasangan.add(kunci to nilai)
        }
    }
}

fun main() {
    val tumpukan = Tumpukan(kapasitas = 3)
    val item1 = tumpukan.Item("Budi")
    val item2 = tumpukan.Item("Siti")
    val item3 = tumpukan.Item("Dewi")
    val item4 = tumpukan.Item("Andi")

    item1.dorong()
    item2.dorong()
    item3.dorong()
    item4.dorong()  // akan tertolak karena tumpukan penuh
    println()
    tumpukan.cetakSemua()
    println()

    // Akses instance outer dari dalam inner class.
    val outer = item1.aksesOuter()
    println("outer === tumpukan: ${outer === tumpukan}")
    println()

    val kata = Kata("belajar-kode")
    val penghitung = kata.PenghitungHuruf()
    println("Jumlah huruf 'a' dalam 'belajar-kode': ${penghitung.hitung('a')}")
    println("Jumlah huruf 'e' dalam 'belajar-kode': ${penghitung.hitung('e')}")
    println("Jumlah huruf 'z' dalam 'belajar-kode': ${penghitung.hitung('z')}")
    println("Method hitung dipanggil sebanyak: ${penghitung.berapaKaliDihitung()}")
    println()

    val peta = PetaWarna()
    val pencariMerah = peta.Pencari("merah")
    pencariMerah.tambahkan("#FF0000")
    val pencariHijau = peta.Pencari("hijau")
    pencariHijau.tambahkan("#00FF00")
    println("Cari 'merah' di peta: ${peta.Pencari("merah").temukan()}")
    println("Cari 'biru' di peta: ${peta.Pencari("biru").temukan()}")
}

Output yang Dihasilkan:

text
Menambahkan 'Budi' (isi: 1/3)
Menambahkan 'Siti' (isi: 2/3)
Menambahkan 'Dewi' (isi: 3/3)
Tumpukan penuh, tidak bisa menambah 'Andi'

Isi tumpukan: Budi, Siti, Dewi

outer === tumpukan: true

Jumlah huruf 'a' dalam 'belajar-kode': 2
Jumlah huruf 'e' dalam 'belajar-kode': 2
Jumlah huruf 'z' dalam 'belajar-kode': 0
Method hitung dipanggil sebanyak: 3

Cari 'merah' di peta: #FF0000
Cari 'biru' di peta: null

Mari kita bedah kode di atas:

  1. inner class Item(private val nilai: String): keyword inner membuat class Item membawa referensi ke instance Tumpukan yang membuatnya. Tanpa inner, kode penyimpanan.size akan gagal dikompilasi karena penyimpanan adalah property instance milik Tumpukan.
  2. val item1 = tumpukan.Item("Budi"): instance Item dibuat lewat instance outer. Tidak ada sintaks Tumpukan.Item("Budi") tanpa instance outer, karena referensi outer wajib ada.
  3. if (penyimpanan.size >= kapasitas): di dalam method dorong(), kedua nama penyimpanan dan kapasitas adalah property milik outer Tumpukan, namun dapat dipanggil langsung tanpa prefiks karena inner class membawa referensi outer.
  4. penyimpanan.add(nilai): inner class tidak hanya membaca state outer, tetapi juga dapat memodifikasinya. Setelah Item.dorong() dipanggil, list penyimpanan di instance Tumpukan berubah.
  5. fun aksesOuter(): Tumpukan = this@Tumpukan: sintaks this@Outer mengembalikan instance outer dari dalam inner class. Output menunjukkan outer === tumpukan bernilai true, membuktikan keduanya adalah instance yang sama.
  6. dihitung++ di dalam PenghitungHuruf.hitung(): inner class memodifikasi var dihitung milik outer. Setiap pemanggilan hitung() menambah counter, dan berapaKaliDihitung() mengembalikan nilai counter tersebut — bukti inner class berbagi state dengan outer.
  7. Pencari("merah") dan Pencari("hijau"): dua instance inner class berbagi instance outer PetaWarna. Keduanya menambah dan membaca list pasangan milik outer yang sama, sehingga data yang ditambah pencariMerah dapat ditemukan oleh instance Pencari lain yang dibuat dari outer yang sama.

Akses Eksplisit ke Outer: this@Outer #

Ketika inner class memiliki property dengan nama yang sama dengan property outer, kita perlu membedakan keduanya secara eksplisit. Sintaks this@Outer mengacu pada instance outer class, sehingga [email protected] merujuk ke property outer sedangkan properti (tanpa prefiks) merujuk ke property inner bila ada.

Sintaks this@Outer juga berguna ketika kita ingin mengembalikan instance outer dari method inner class — misalnya untuk fluent builder yang mengembalikan outer setelah konfigurasi selesai.

Kapan Memilih Inner vs Nested #

Perbedaan utama dapat diringkas dalam tabel berikut:

AspekNested class (default)Inner class (inner class)
Referensi outerTidakYa
Akses property instance outerTidakYa
Membuat instance tanpa outerBisaTidak
Beban memoriLebih ringanLebih berat

Pilih inner class hanya ketika helper benar-benar perlu membaca atau memodifikasi state outer. Untuk helper murni yang tidak peduli state outer, nested class biasa adalah pilihan yang lebih ringan.

Referensi Lanjutan #

  • Nested classes — bagian inner class yang membawa referensi instance outer

Ringkasan #

Inner class dengan keyword inner membawa referensi implisit ke instance outer, sehingga inner dapat membaca dan memodifikasi anggota instance outer secara langsung. Instance inner wajib dibuat lewat instance outer via outer.Inner(), dan sintaks this@Outer memberi akses eksplisit ke instance outer dari dalam inner class. Pada chapter object Declarations berikutnya, kita akan melihat cara mendeklarasikan singleton langsung di tingkat class dengan keyword object.