Interface terdengar seperti kontrak yang beku: sekali dipublikasikan, menambah satu method baru berarti memaksa seluruh class implementor menulis implementasi — kalau tidak, program mereka gagal kompilasi. Bayangkan pustaka standar Java yang dipakai jutaan class; menambah satu method ke Collection sudah cukup merusak semuanya. Java 8 memperkenalkan default method untuk mengatasi kebuntuan tersebut: method interface yang punya body dan diwarisi semua implementor lama secara otomatis. Java 8 juga membuka static method di dalam interface, dan Java 9 menambahkan private method sebagai helper internal. Pada Chapter ini kita akan melihat ketiganya bekerja, termasuk konflik diamond yang muncul ketika dua interface membawa default method bernama sama.

Masalah Evolusi Interface #

Sebelum Java 8, interface hanya boleh memuat method abstrak. Aturan tersebut membuat evolusi API menjadi mahal: begitu interface Peminjaman dipakai oleh banyak class di berbagai modul, menambah method perpanjang() berupa method abstrak baru memaksa setiap class implementor diperbarui. Pustaka standar menghadapi masalah ini secara nyata saat lambda dan Stream API dirancang — Collection perlu method stream() tanpa merusak ribuan implementasi collection yang sudah ada.

Solusinya: stream() pada java.util.Collection dideklarasikan sebagai default method, sehingga semua implementasi lama otomatis mendapat method baru tanpa satu baris perubahan kode. Mekanisme inilah yang akan kita tiru dengan interface buatan sendiri.

Default Method Menyelamatkan Implementor Lama #

Interface Peminjaman berikut dipakai class BukuLama yang hanya mengimplementasikan pinjam(). Kita menambahkan default method perpanjang(), static method dendaPerHari(), dan private method hitungTambahan() sebagai helper:

interface Peminjaman {
    int LAMA_PINJAM_HARI = 14;

    void pinjam();

    // Default method: implementasi bawaan yang diwarisi implementor lama.
    default int perpanjang() {
        return hitungTambahan(LAMA_PINJAM_HARI, 7);
    }

    // Static method: helper yang melekat pada interface, bukan pada objek.
    static int dendaPerHari() {
        return 1000;
    }

    // Private method (Java 9+): helper bersama untuk method default.
    private int hitungTambahan(int dasar, int tambahan) {
        return dasar + tambahan;
    }
}

// Implementor lama: hanya mengimplementasikan pinjam().
// Default method perpanjang() diwarisi apa adanya tanpa mengubah class ini.
class BukuLama implements Peminjaman {
    private final String judul;

    public BukuLama(String judul) {
        this.judul = judul;
    }

    @Override
    public void pinjam() {
        System.out.println(judul + " dipinjam");
    }
}

// Implementor baru: mengganti default dengan aturan miliknya sendiri.
class Majalah implements Peminjaman {
    private final String judul;

    public Majalah(String judul) {
        this.judul = judul;
    }

    @Override
    public void pinjam() {
        System.out.println(judul + " dipinjam");
    }

    @Override
    public int perpanjang() {
        return LAMA_PINJAM_HARI;
    }
}

public class EvolusiPeminjaman {
    public static void main(String[] args) {
        BukuLama buku = new BukuLama("Java Dasar");
        buku.pinjam();
        System.out.println("Perpanjangan buku: " + buku.perpanjang() + " hari");

        Majalah majalah = new Majalah("Majalah Teknologi");
        majalah.pinjam();
        System.out.println("Perpanjangan majalah: " + majalah.perpanjang() + " hari");

        // Static method dipanggil lewat nama interface, tanpa objek.
        System.out.println("Denda per hari: Rp" + Peminjaman.dendaPerHari());
    }
}

Output yang Dihasilkan:

text
Java Dasar dipinjam
Perpanjangan buku: 21 hari
Majalah Teknologi dipinjam
Perpanjangan majalah: 14 hari
Denda per hari: Rp1000

Mari kita bedah bagian-bagian pentingnya:

  1. Baris 7-9 (default int perpanjang()): kata kunci default memberi method ini sebuah body. Implementor yang tidak menulis ulang mewarisi body tersebut — BukuLama membuktikannya dengan mencetak 21 hari (14 + 7).
  2. Baris 12-14 (static int dendaPerHari()): static method interface dipanggil lewat nama interface, seperti Peminjaman.dendaPerHari(). Method ini tidak diwarisi class implementor, sehingga tidak bisa dipanggil lewat objek.
  3. Baris 17-19 (private int hitungTambahan(...)): private method (Java 9+) hanya bisa dipanggil method lain pada interface yang sama; kegunaannya memecah kode default method yang panjang tanpa mengekspos helper ke implementor.
  4. class BukuLama: class ini tidak menyentuh perpanjang() sama sekali. Inilah bukti evolusi API — interface bertambah method, implementor lama tetap kompilasi dan berperilaku benar.
  5. class Majalah: @Override public int perpanjang() mengganti default dengan aturan khusus majalah (tidak ada tambahan hari), sehingga mencetak 14 hari.

Konflik Default Method Ala Diamond #

Kekuatan default method membawa satu konsekuensi: dua interface bisa membawa default method dengan nama dan tanda tangan yang sama persis. Kedua interface berikut masing-masing punya diskon():

java DapatDiskon.java
// Interface pertama dengan method default.
interface DapatDiskon {
    default double diskon() {
        return 0.05;
    }
}
// Interface kedua dengan method default bernama sama.
interface AnggotaPremium {
    default double diskon() {
        return 0.15;
    }
}

Jika sebuah class mengimplementasikan keduanya tanpa intervensi, compiler tidak bisa menebak default mana yang harus dipakai — kondisi ini disebut konflik diamond. Berkas berikut membuktikannya:

public class KonflikDefault implements DapatDiskon, AnggotaPremium {
    public static void main(String[] args) {
        // Baris ini tidak pernah tercapai: kompilasi sudah gagal lebih dulu.
        System.out.println("Diskon: " + new KonflikDefault().diskon());
    }
}

Compiler javac mengeluarkan pesan berikut:

text
error: types DapatDiskon and AnggotaPremium are incompatible;
  class KonflikDefault inherits unrelated defaults for diskon()
  from types DapatDiskon and AnggotaPremium

Mendamaikan Konflik dengan Override #

Satu-satunya jalan keluar: class implementor harus meng-override method yang bertabrakan dan memutuskan sendiri. Sintaks NamaInterface.super.method() memungkinkan kita memanggil versi default dari interface tertentu:

class Transaksi implements DapatDiskon, AnggotaPremium {
    private final double total;

    public Transaksi(double total) {
        this.total = total;
    }

    // Override wajib: dua default yang bersaing harus didamaikan manual.
    @Override
    public double diskon() {
        return Math.max(DapatDiskon.super.diskon(), AnggotaPremium.super.diskon());
    }

    public double totalBayar() {
        return total * (1 - diskon());
    }
}

public class ResolusiKonflik {
    public static void main(String[] args) {
        Transaksi t = new Transaksi(100000);
        System.out.printf("Diskon dipilih: %.0f%%%n", t.diskon() * 100);
        System.out.printf("Total bayar: Rp%.0f%n", t.totalBayar());
    }
}

Output yang Dihasilkan:

text
Diskon dipilih: 15%
Total bayar: Rp85000

Mari kita bedah dua baris kuncinya:

  1. @Override public double diskon(): kehadiran override ini menghapus ambiguitas — compiler kembali menerima class tersebut. Tanpa method ini, compile error inherits unrelated defaults tetap muncul.
  2. Math.max(DapatDiskon.super.diskon(), AnggotaPremium.super.diskon()): notasi Interface.super.method() mengambil body default milik interface spesifik. Kode ini memilih diskon paling menguntungkan anggota (0.15), sehingga transaksi Rp100.000 cukup membayar Rp85.000.

Default dan static method menjadikan interface kontrak yang bisa tumbuh dengan aman. Namun fleksibilitas pewarisan seperti ini juga menggoda kita menumpuk kemampuan lewat hierarki — padahal tidak semua hubungan antar class layak dimodelkan dengan pewarisan. Chapter Composition vs Inheritance akan menguji kapan extends justru menjadi beban.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

AnggotaPremium.java

Unduh berkas
java
// Interface kedua dengan method default bernama sama.
interface AnggotaPremium {
    default double diskon() {
        return 0.15;
    }
}

DapatDiskon.java

Unduh berkas
java
// Interface pertama dengan method default.
interface DapatDiskon {
    default double diskon() {
        return 0.05;
    }
}

EvolusiPeminjaman.java

Unduh berkas
java
interface Peminjaman {
    int LAMA_PINJAM_HARI = 14;

    void pinjam();

    // Default method: implementasi bawaan yang diwarisi implementor lama.
    default int perpanjang() {
        return hitungTambahan(LAMA_PINJAM_HARI, 7);
    }

    // Static method: helper yang melekat pada interface, bukan pada objek.
    static int dendaPerHari() {
        return 1000;
    }

    // Private method (Java 9+): helper bersama untuk method default.
    private int hitungTambahan(int dasar, int tambahan) {
        return dasar + tambahan;
    }
}

// Implementor lama: hanya mengimplementasikan pinjam().
// Default method perpanjang() diwarisi apa adanya tanpa mengubah class ini.
class BukuLama implements Peminjaman {
    private final String judul;

    public BukuLama(String judul) {
        this.judul = judul;
    }

    @Override
    public void pinjam() {
        System.out.println(judul + " dipinjam");
    }
}

// Implementor baru: mengganti default dengan aturan miliknya sendiri.
class Majalah implements Peminjaman {
    private final String judul;

    public Majalah(String judul) {
        this.judul = judul;
    }

    @Override
    public void pinjam() {
        System.out.println(judul + " dipinjam");
    }

    @Override
    public int perpanjang() {
        return LAMA_PINJAM_HARI;
    }
}

public class EvolusiPeminjaman {
    public static void main(String[] args) {
        BukuLama buku = new BukuLama("Java Dasar");
        buku.pinjam();
        System.out.println("Perpanjangan buku: " + buku.perpanjang() + " hari");

        Majalah majalah = new Majalah("Majalah Teknologi");
        majalah.pinjam();
        System.out.println("Perpanjangan majalah: " + majalah.perpanjang() + " hari");

        // Static method dipanggil lewat nama interface, tanpa objek.
        System.out.println("Denda per hari: Rp" + Peminjaman.dendaPerHari());
    }
}

KonflikDefault.java

Unduh berkas
java
public class KonflikDefault implements DapatDiskon, AnggotaPremium {
    public static void main(String[] args) {
        // Baris ini tidak pernah tercapai: kompilasi sudah gagal lebih dulu.
        System.out.println("Diskon: " + new KonflikDefault().diskon());
    }
}

ResolusiKonflik.java

Unduh berkas
java
class Transaksi implements DapatDiskon, AnggotaPremium {
    private final double total;

    public Transaksi(double total) {
        this.total = total;
    }

    // Override wajib: dua default yang bersaing harus didamaikan manual.
    @Override
    public double diskon() {
        return Math.max(DapatDiskon.super.diskon(), AnggotaPremium.super.diskon());
    }

    public double totalBayar() {
        return total * (1 - diskon());
    }
}

public class ResolusiKonflik {
    public static void main(String[] args) {
        Transaksi t = new Transaksi(100000);
        System.out.printf("Diskon dipilih: %.0f%%%n", t.diskon() * 100);
        System.out.printf("Total bayar: Rp%.0f%n", t.totalBayar());
    }
}