Interface

Interface #

Interface adalah kontrak — ia mendefinisikan apa yang harus bisa dilakukan oleh sebuah kelas, tanpa menentukan bagaimana melakukannya. Sebuah kelas yang mengimplementasikan interface berjanji untuk menyediakan implementasi dari semua method abstrak yang didefinisikan di dalamnya. Interface adalah fondasi dari desain yang fleksibel dan mudah diuji: daripada bergantung pada implementasi konkret, kode yang baik bergantung pada interface — sehingga implementasi bisa diganti kapan saja tanpa mengubah kode yang bergantung padanya. Artikel ini membahas semua aspek interface di Kotlin, dari deklarasi dasar hingga pola-pola desain penting seperti multiple inheritance, konflik diamond, dan delegation.

Mendefinisikan Interface #

Interface dideklarasikan dengan kata kunci interface. Semua method di dalamnya secara default adalah abstrak (tanpa implementasi), kecuali yang secara eksplisit diberi body.

interface Kendaraan {
    // Properti abstrak — harus diimplementasikan
    val merek: String
    val kecepatanMaksKmh: Int

    // Method abstrak — harus diimplementasikan
    fun nyalakanMesin()
    fun matikanMesin()

    // Method dengan implementasi default — boleh di-override, boleh tidak
    fun klakson() {
        println("Beep beep!")
    }

    fun info(): String = "$merek (maks ${kecepatanMaksKmh}km/h)"
}

Tiga hal penting tentang interface: pertama, interface tidak bisa menyimpan state (tidak ada backing field untuk properti). Kedua, semua member interface secara default public. Ketiga, sebuah kelas bisa mengimplementasikan banyak interface sekaligus.


Mengimplementasikan Interface #

Kelas yang mengimplementasikan interface menggunakan sintaks : NamaInterface — sama seperti inheritance, tapi untuk interface tidak butuh tanda kurung.

class Mobil(
    override val merek: String,
    override val kecepatanMaksKmh: Int,
    val jenisTransmisi: String
) : Kendaraan {

    private var mesinMenyala = false

    override fun nyalakanMesin() {
        if (!mesinMenyala) {
            mesinMenyala = true
            println("$merek: Mesin menyala — vroom!")
        } else {
            println("$merek: Mesin sudah menyala")
        }
    }

    override fun matikanMesin() {
        if (mesinMenyala) {
            mesinMenyala = false
            println("$merek: Mesin dimatikan")
        }
    }

    // Tidak override klakson() — pakai implementasi default dari interface
    // Tidak override info() — pakai implementasi default dari interface
}

val avanza = Mobil("Toyota Avanza", 160, "Otomatis")
avanza.nyalakanMesin()    // Toyota Avanza: Mesin menyala — vroom!
avanza.klakson()          // Beep beep!
println(avanza.info())    // Toyota Avanza (maks 160km/h)

Properti di Interface #

Interface bisa mendeklarasikan properti, tapi tidak bisa menyimpan nilainya (tidak ada backing field). Ada dua cara kelas mengimplementasikan properti interface: dengan properti biasa di constructor, atau dengan getter kustom.

interface BisaDigambar {
    val warna: String
    val tebalGaris: Double
        get() = 1.0  // nilai default via getter — boleh di-override

    fun gambar()
    fun hitung(): String
}

class Persegi(
    override val warna: String,
    val sisi: Double
) : BisaDigambar {

    // tebalGaris tidak di-override — pakai default 1.0

    override fun gambar() {
        println("Menggambar persegi sisi=${sisi}cm warna=$warna")
    }

    override fun hitung(): String {
        val luas = sisi * sisi
        val keliling = 4 * sisi
        return "Luas=${luas}cm², Keliling=${keliling}cm"
    }
}

class Lingkaran(
    override val warna: String,
    val radius: Double,
    override val tebalGaris: Double = 2.5  // override nilai default
) : BisaDigambar {

    override fun gambar() {
        println("Menggambar lingkaran r=${radius}cm warna=$warna garis=${tebalGaris}px")
    }

    override fun hitung(): String {
        val luas = Math.PI * radius * radius
        val keliling = 2 * Math.PI * radius
        return "Luas=${"%.2f".format(luas)}cm², Keliling=${"%.2f".format(keliling)}cm"
    }
}

val bentuk: List<BisaDigambar> = listOf(
    Persegi("Merah", 5.0),
    Lingkaran("Biru", 3.0)
)

bentuk.forEach {
    it.gambar()
    println(it.hitung())
    println()
}

Interface sebagai Tipe #

Salah satu kegunaan terpenting interface adalah sebagai tipe — kode yang bergantung pada interface, bukan implementasi konkret. Ini membuat kode mudah diuji dan mudah diganti implementasinya.

interface Repository<T> {
    fun cariById(id: Long): T?
    fun simpanSemua(items: List<T>): Boolean
    fun hapus(id: Long): Boolean
    fun ambilSemua(): List<T>
}

data class Produk(val id: Long, val nama: String, val harga: Double)

// Implementasi yang menyimpan data di memori (untuk testing)
class ProdukRepositoryMemori : Repository<Produk> {
    private val data = mutableMapOf<Long, Produk>()

    override fun cariById(id: Long) = data[id]

    override fun simpanSemua(items: List<Produk>): Boolean {
        items.forEach { data[it.id] = it }
        return true
    }

    override fun hapus(id: Long): Boolean = data.remove(id) != null

    override fun ambilSemua() = data.values.toList()
}

// Implementasi yang menyimpan ke database (production)
class ProdukRepositoryDatabase(private val koneksi: String) : Repository<Produk> {
    override fun cariById(id: Long): Produk? {
        println("Query: SELECT * FROM produk WHERE id=$id via $koneksi")
        return null // placeholder
    }
    override fun simpanSemua(items: List<Produk>): Boolean {
        println("INSERT ${items.size} produk via $koneksi")
        return true
    }
    override fun hapus(id: Long): Boolean {
        println("DELETE FROM produk WHERE id=$id via $koneksi")
        return true
    }
    override fun ambilSemua(): List<Produk> {
        println("SELECT * FROM produk via $koneksi")
        return emptyList()
    }
}

// Layanan yang bergantung pada interface, bukan implementasi
class LayananProduk(private val repo: Repository<Produk>) {
    fun tambahProduk(produk: Produk) {
        repo.simpanSemua(listOf(produk))
    }

    fun cariProduk(id: Long): Produk? = repo.cariById(id)

    fun daftarProduk() = repo.ambilSemua()
}

// Di production — pakai database
val layananProd = LayananProduk(ProdukRepositoryDatabase("jdbc:postgresql://..."))

// Di test — pakai in-memory
val layananTest = LayananProduk(ProdukRepositoryMemori())
layananTest.tambahProduk(Produk(1, "Laptop", 15_000_000.0))
println(layananTest.daftarProduk())

Multiple Inheritance — Implementasi Beberapa Interface #

Kelas di Kotlin hanya bisa mewarisi satu kelas, tapi bisa mengimplementasikan banyak interface sekaligus. Ini adalah cara Kotlin mencapai fleksibilitas tanpa kompleksitas multiple class inheritance.

interface BisaBerbicara {
    fun bicara(pesan: String)
}

interface BisaBerjalan {
    fun jalan(langkah: Int)
    fun posisiSaatIni(): String = "Tidak diketahui"
}

interface BisaBernyanyi {
    fun nyanyi(lagu: String)
    fun volume(): Int = 5  // volume default 5
}

class Manusia(val nama: String) : BisaBerbicara, BisaBerjalan, BisaBernyanyi {

    private var posisi = 0

    override fun bicara(pesan: String) {
        println("$nama berkata: \"$pesan\"")
    }

    override fun jalan(langkah: Int) {
        posisi += langkah
        println("$nama berjalan $langkah langkah (posisi: $posisi)")
    }

    override fun posisiSaatIni() = "Posisi $posisi"  // override implementasi default

    override fun nyanyi(lagu: String) {
        println("$nama menyanyikan '$lagu' dengan volume ${volume()}")
    }

    // volume() tidak di-override — pakai default 5
}

val budi = Manusia("Budi")
budi.bicara("Selamat pagi!")
budi.jalan(10)
budi.nyanyi("Garuda Pancasila")
println(budi.posisiSaatIni())

Konflik Diamond — Method Default yang Sama dari Dua Interface #

Ketika dua interface menyediakan implementasi default untuk method dengan nama yang sama, kelas yang mengimplementasikan keduanya harus secara eksplisit menyelesaikan konflik ini.

interface A {
    fun halo() = println("Halo dari A")
    fun salam() = println("Salam dari A")
}

interface B {
    fun halo() = println("Halo dari B")
    fun salam() = println("Salam dari B")
}

// ANTI-PATTERN: tidak menyelesaikan konflik — error kompilasi
// class C : A, B {
//     // halo() dari A atau B? Compiler tidak tahu
// }

// BENAR: selesaikan konflik secara eksplisit
class C : A, B {
    // Wajib override karena ada konflik
    override fun halo() {
        super<A>.halo()  // panggil implementasi A
        super<B>.halo()  // panggil implementasi B
        println("Halo dari C sendiri")
    }

    // Pilih salah satu, atau buat implementasi baru
    override fun salam() {
        super<B>.salam()  // pilih B saja
    }
}

val c = C()
c.halo()
// Halo dari A
// Halo dari B
// Halo dari C sendiri
c.salam()
// Salam dari B

Interface dengan Generics #

Interface bisa menjadi generik — mendefinisikan kontrak yang bekerja untuk berbagai tipe data.

interface Transformasi<I, O> {
    fun ubah(input: I): O
    fun ubahSemua(inputs: List<I>): List<O> = inputs.map { ubah(it) }
}

class StringKeInt : Transformasi<String, Int> {
    override fun ubah(input: String): Int = input.toIntOrNull() ?: 0
}

class IntKeBiner : Transformasi<Int, String> {
    override fun ubah(input: Int): String = Integer.toBinaryString(input)
}

val parser = StringKeInt()
println(parser.ubah("42"))           // 42
println(parser.ubah("bukan angka"))  // 0
println(parser.ubahSemua(listOf("1", "2", "3", "abc")))  // [1, 2, 3, 0]

val biner = IntKeBiner()
println(biner.ubahSemua(listOf(1, 5, 10, 255)))  // [1, 101, 1010, 11111111]

Delegation Pattern dengan by #

Kotlin mendukung interface delegation secara built-in menggunakan kata kunci by. Ini memungkinkan kelas mendelegasikan implementasi interface ke objek lain tanpa menulis boilerplate forwarding method secara manual.

interface Penyimpan {
    fun simpan(kunci: String, nilai: String)
    fun ambil(kunci: String): String?
    fun hapus(kunci: String)
}

// Implementasi konkret
class PenyimpanMemori : Penyimpan {
    private val data = mutableMapOf<String, String>()
    override fun simpan(kunci: String, nilai: String) { data[kunci] = nilai }
    override fun ambil(kunci: String) = data[kunci]
    override fun hapus(kunci: String) { data.remove(kunci) }
}

// Delegasi ke PenyimpanMemori — tidak perlu override semua method
class PenyimpanDenganLog(private val delegate: Penyimpan) : Penyimpan by delegate {
    // Hanya override method yang perlu tambahan perilaku
    override fun simpan(kunci: String, nilai: String) {
        println("[LOG] Menyimpan kunci='$kunci'")
        delegate.simpan(kunci, nilai)
    }

    override fun hapus(kunci: String) {
        println("[LOG] Menghapus kunci='$kunci'")
        delegate.hapus(kunci)
    }
    // ambil() didelegasikan otomatis ke delegate — tidak perlu ditulis
}

val memori = PenyimpanMemori()
val denganLog = PenyimpanDenganLog(memori)

denganLog.simpan("nama", "Budi")    // [LOG] Menyimpan kunci='nama'
denganLog.simpan("umur", "25")      // [LOG] Menyimpan kunci='umur'
println(denganLog.ambil("nama"))    // Budi — didelegasikan ke memori
denganLog.hapus("umur")             // [LOG] Menghapus kunci='umur'

Tanpa by, kamu harus menulis forwarding method untuk setiap method di interface secara manual — sangat verbose untuk interface dengan banyak method.


Interface vs Abstract Class #

Ini adalah keputusan desain yang sering membingungkan. Panduan singkatnya:

flowchart TD
    A{Perlu menyimpan\nstate / backing field?} -- Ya --> B["Abstract Class"]
    A -- Tidak --> C{Satu kelas perlu\nimplementasi lebih\ndari satu?}
    C -- Ya --> D["Interface\n(multiple inheritance)"]
    C -- Tidak --> E{Mendefinisikan\n'bisa melakukan apa'\natau 'adalah apa'?}
    E -- "Bisa melakukan\n(kemampuan/perilaku)" --> F["Interface\nContoh: BisaTerbang, BisaBerenang"]
    E -- "Adalah apa\n(identitas/jenis)" --> G["Abstract Class\nContoh: Hewan, Kendaraan"]

Tabel perbandingan:

AspekInterfaceAbstract Class
StateTidak bisa (tidak ada backing field)Bisa punya properti dengan state
Multiple inheritanceBisa mengimplementasikan banyakHanya bisa mewarisi satu
ConstructorTidak punyaPunya constructor
Visibilitas memberSemua publicBisa private, protected, dll
Kata kunci implementasi: NamaInterface (tanpa kurung): NamaKelas() (dengan kurung)
Cocok untukKemampuan/perilaku, kontrak APIHierarki “adalah jenis dari”
// Interface — mendefinisikan KEMAMPUAN
interface BisaEksporCsv {
    fun eksporKeCsv(): String
}

interface BisaEksporPdf {
    fun eksporKePdf(): ByteArray
}

// Abstract class — mendefinisikan IDENTITAS dengan state bersama
abstract class DokumenBisnis(
    val judul: String,
    val tanggalDibuat: Long = System.currentTimeMillis()
) {
    abstract fun buatKonten(): String
    fun metadata() = "Judul: $judul | Dibuat: $tanggalDibuat"
}

// Kelas konkret — IS-A DokumenBisnis, CAN BisaEksporCsv dan BisaEksporPdf
class LaporanPenjualan(
    judul: String,
    private val dataPenjualan: List<Pair<String, Double>>
) : DokumenBisnis(judul), BisaEksporCsv, BisaEksporPdf {

    override fun buatKonten(): String {
        return dataPenjualan.joinToString("\n") { (produk, nilai) ->
            "$produk: Rp${"%,.0f".format(nilai)}"
        }
    }

    override fun eksporKeCsv(): String {
        return "Produk,Nilai\n" + dataPenjualan.joinToString("\n") { (p, v) -> "$p,$v" }
    }

    override fun eksporKePdf(): ByteArray {
        println("Menghasilkan PDF untuk: $judul")
        return ByteArray(0) // placeholder
    }
}

Ringkasan #

  • Interface adalah kontrak kemampuan — ia mendefinisikan apa yang bisa dilakukan, bukan siapa yang melakukannya. Gunakan interface untuk mendefinisikan perilaku yang bisa dimiliki oleh berbagai jenis kelas yang tidak berkaitan.
  • Implementasi default mengurangi boilerplate — method dengan body di interface memungkinkan kamu menambah method baru ke interface tanpa memaksa semua implementasi yang ada untuk diperbarui.
  • Interface tidak bisa menyimpan state — properti di interface tidak punya backing field. Implementasi harus menyediakan penyimpanannya sendiri, baik via constructor maupun properti kelas.
  • Multiple interface untuk multiple kemampuan — sebuah kelas bisa mengimplementasikan banyak interface, memungkinkan komposisi kemampuan yang fleksibel tanpa kerumitan multiple inheritance dari kelas.
  • Konflik diamond wajib diselesaikan — jika dua interface punya method default dengan nama yang sama, kelas yang mengimplementasikan keduanya wajib meng-override method itu dan menentukan perilaku yang diinginkan. Gunakan super<NamaInterface>.method() untuk memanggil implementasi spesifik.
  • Interface sebagai tipe memungkinkan loose coupling — kode yang bergantung pada interface, bukan implementasi konkret, jauh lebih mudah diuji (bisa di-mock) dan lebih mudah diganti implementasinya.
  • Delegation dengan by — gunakan by untuk mendelegasikan implementasi interface ke objek lain tanpa menulis forwarding method secara manual. Ideal untuk pola Decorator.
  • Interface untuk “bisa melakukan”, abstract class untuk “adalah jenis” — gunakan interface ketika mendefinisikan kemampuan/perilaku lintas hierarki. Gunakan abstract class ketika mendefinisikan identitas dengan state bersama.

← Sebelumnya: Kelas   Berikutnya: Eksepsi →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact