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:
| Aspek | Interface | Abstract Class |
|---|---|---|
| State | Tidak bisa (tidak ada backing field) | Bisa punya properti dengan state |
| Multiple inheritance | Bisa mengimplementasikan banyak | Hanya bisa mewarisi satu |
| Constructor | Tidak punya | Punya constructor |
| Visibilitas member | Semua public | Bisa private, protected, dll |
| Kata kunci implementasi | : NamaInterface (tanpa kurung) | : NamaKelas() (dengan kurung) |
| Cocok untuk | Kemampuan/perilaku, kontrak API | Hierarki “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— gunakanbyuntuk 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.