Metode Default dan Static

Mengembangkan interface tanpa merusak implementasi lewat metode default Java 8 dan aturan resolusi diamond.

Daftar isi

Sejak Java 8, interface boleh memuat metode yang sudah memiliki isi, yaitu metode default dan metode static, sehingga kontrak dapat ditambah perilaku tanpa merusak kelas-kelas yang sudah mengimplementasikannya. Topik ini menunjukkan cara menulis keduanya, kegunaan masing-masing, serta aturan yang menengahi konflik ketika satu kelas mewarisi metode default yang sama dari dua interface sekaligus, bentuk konflik yang lazim disebut persoalan diamond.

Masalah evolusi interface#

Kontrak yang murni memiliki kelemahan yang baru terasa setelah program dipakai bertahun-tahun. Menambah satu metode abstrak ke interface yang sudah dipakai banyak kelas memaksa seluruh kelas tersebut menyediakan isinya, karena kelas yang tidak mengisinya langsung gagal kompilasi. Pustaka standar Java menghadapi persoalan ini pada 2014, ketika Collections Framework perlu menambah metode baru, misalnya sort pada List, tanpa mematikan jutaan baris kode yang mengimplementasikan interface lamanya. Solusinya adalah mengizinkan metode di interface membawa isi, dengan dua bentuk yang berbeda tujuan, yaitu metode default yang diwarisi pengimplementasinya dan metode static yang hidup di interface itu sendiri.

Metode default#

Metode default ditulis dengan kata kunci default diikuti badan metode yang utuh, dan setiap kelas pengimplementasi mewarisinya tanpa harus menulis apa pun. Pola yang paling sering berguna adalah default yang menyusun perilaku di atas metode abstrak milik interface itu sendiri, sehingga kelas cukup mengisi bagian yang benar-benar berbeda. Program berikut menambahkan metode kosong() pada kontrak antrean, dan implementasinya cukup menghitung hasil ukuran() yang sudah dijanjikan.

java DemoAntrean.java
import java.util.ArrayList;

public class DemoAntrean {

    public static void main(String[] args) {
        AntreanLoket loket = new AntreanLoket();
        loket.ambil("A-101");
        loket.ambil("A-102");
        AntreanPiket piket = new AntreanPiket();

        System.out.println("Antrean loket kosong? " + loket.kosong());
        System.out.println("Antrean piket kosong? " + piket.kosong());
    }
}

interface AntreanTugas {
    int ukuran();

    default boolean kosong() {
        return ukuran() == 0;
    }
}

class AntreanLoket implements AntreanTugas {
    ArrayList<String> nomor = new ArrayList<>();

    void ambil(String nomorBaru) {
        nomor.add(nomorBaru);
    }

    public int ukuran() {
        return nomor.size();
    }
}

class AntreanPiket implements AntreanTugas {
    public int ukuran() {
        return 0;
    }
}
text Keluaran
Antrean loket kosong? false
Antrean piket kosong? true

Kedua kelas hanya menulis ukuran(), sedangkan kosong() datang dari interface dan memanggil ukuran() milik objek yang bersangkutan, sehingga jawabannya mengikuti dynamic dispatch. Kelas yang butuh perilaku berbeda tetap boleh mengoverride metode default itu seperti mengoverride metode biasa, dan panggilan NamaInterface.super.metode() dari dalam badan override akan meneruskan pekerjaan ke versi default. Cara ini menjaga interface tetap boleh bertambah tanpa mengubah satu baris pun pada kelas-kelas lamanya.

Satu larangan perlu dicatat, yaitu metode default tidak boleh menimpa metode milik java.lang.Object seperti toString() atau equals(), dan kompiler menolaknya karena setiap kelas sudah membawa versi konkret milik Object. Batas ini menjaga perilaku dasar setiap objek tetap seragam, tidak bergantung pada interface yang kebetulan dipakai kelasnya.

Metode static di interface#

Metode static di interface membawa isi tetapi tidak diwarisi kelas pengimplementasinya, sehingga satu-satunya cara memanggilnya adalah lewat nama interface itu sendiri. Tempat alaminya adalah metode pembantu yang berhubungan langsung dengan kontrak, misalnya penghitungan statistik atas sekumpulan nilai, yang sebelumnya lazim ditempelkan pada kelas utilitas terpisah. Contoh berikut menaruh dua metode pembantu di dalam interface Statistik.

java UjiStatistik.java
public class UjiStatistik {

    public static void main(String[] args) {
        int[] nilai = {80, 95, 70, 85};

        System.out.println("Rata-rata: " + Statistik.rataRata(nilai));
        System.out.println("Nilai terbesar: " + Statistik.maksimum(nilai));
    }
}

interface Statistik {
    static double rataRata(int[] nilai) {
        int total = 0;
        for (int n : nilai) {
            total = total + n;
        }
        return (double) total / nilai.length;
    }

    static int maksimum(int[] nilai) {
        int terbesar = nilai[0];
        for (int n : nilai) {
            if (n > terbesar) {
                terbesar = n;
            }
        }
        return terbesar;
    }
}
text Keluaran
Rata-rata: 82.5
Nilai terbesar: 95

Panggilan Statistik.rataRata(nilai) sah karena menuju nama interfacenya, sedangkan pemanggilan lewat kelas pengimplementasi tidak ada artinya, karena metode static interface tidak menjadi anggota kelas tersebut. Potongan berikut membandingkan bentuk yang sah dengan bentuk yang ditolak kompiler.

java Contoh: metode static interface tidak diwarisi
// tidak-dijalankan
Statistik.rataRata(nilai);     // sah, dipanggil lewat nama interface
Statistik.maksimum(nilai);     // sah juga
UjiStatistik.rataRata(nilai);  // error: cannot find symbol

Karena tidak diwarisi, metode static interface tidak dapat dioverride, dan perubahan perilakunya hanya mungkin dilakukan pada interfacenya sendiri.

Aturan resolusi diamond#

Bila satu kelas mengimplementasikan dua interface yang sama-sama memuat metode default bertanda tangan sama, kompiler tidak boleh menebak salah satu, sehingga kelas itu wajib mengoverride metodenya dan menentukan hasil akhirnya secara eksplisit. Di dalam badan override, NamaInterface.super.metode() meneruskan pekerjaan ke versi interface tertentu, dan super.metode() tanpa awalan meneruskan ke versi kelas induk bila ada. Bentuk konflik ini dikenal sebagai persoalan diamond, karena jalur pewarisannya menyerupai belah ketupat.

java DemoDiamond.java
public class DemoDiamond {

    public static void main(String[] args) {
        LaporanHarian laporan = new LaporanHarian();

        System.out.println(laporan.label());
    }
}

interface Laporan {
    default String label() {
        return "Laporan";
    }
}

interface Arsip {
    default String label() {
        return "Arsip";
    }
}

class LaporanHarian implements Laporan, Arsip {
    @Override
    public String label() {
        return Laporan.super.label() + " harian";
    }
}
text Keluaran
Laporan harian

Menghapus override pada LaporanHarian membuat kompilasi berhenti dengan pesan yang menjelaskan konfliknya, sebagaimana diperlihatkan potongan berikut.

java Contoh: diamond tanpa override ditolak kompiler
// tidak-dijalankan
class LaporanHarian implements Laporan, Arsip {
}
// error: class LaporanHarian inherits unrelated defaults for label()
//        from types Laporan and Arsip

Ada satu aturan tambahan yang menyelesaikan sebagian konflik tanpa penulisan apa pun, yaitu kelas selalu menang atas interface. Bila kelas induk yang konkret sudah menyediakan metode dengan tanda tangan yang sama, versi induk itulah yang diwarisi turunannya, dan metode default interfacenya terlewati begitu saja. Aturan ini menjaga perilaku yang sudah ada di kelas tidak berubah diam-diam hanya karena kelasnya memakai interface baru.

Kesalahan umum#

  • Menganggap metode default membuat interface boleh menyimpan keadaan. Field instance tetap dilarang di interface, sehingga metode default hanya boleh bertumpu pada metode abstrak, konstanta, atau parameternya sendiri. Keadaan milik objek tetap ditempatkan di kelas pengimplementasinya.
  • Memanggil metode static interface lewat nama kelas. Metode tersebut tidak diwarisi kelas pengimplementasi, sehingga UjiStatistik.rataRata(...) ditolak dengan cannot find symbol. Gunakan Statistik.rataRata(...) sesuai tempat metodenya berada.
  • Menyisakan konflik diamond tanpa override. Dua default yang bertabrakan menghentikan kompilasi sampai kelasnya menuliskan resolusinya sendiri. Pilih versi interface dengan NamaInterface.super.metode() atau tulis logika penggantinya.
  • Mengoverride default tetapi memanggilnya secara rekursif. Menulis label() di dalam badan label() sendiri membuat pemanggilan berputar tanpa henti sampai StackOverflowError. Teruskan panggilan ke NamaInterface.super.label() bila versi default tetap ingin dipakai.

Ringkasan#

  • Metode default memberi isi pada metode interface dan diwarisi seluruh kelas pengimplementasinya, sehingga interface dapat bertambah tanpa merusak kode lama.
  • Default yang menyusun metode abstrak interface sendiri menjaga kelas tetap hanya mengisi bagian yang berbeda.
  • Metode static interface membawa isi tetapi tidak diwarisi, dan hanya dapat dipanggil lewat nama interfacenya.
  • Konflik dua default yang sama wajib dipecahkan kelasnya melalui override, dengan NamaInterface.super.metode() sebagai jalur ke versi tertentu, dan kelas induk konkret selalu menang atas default interface.

Referensi#