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.
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;
}
}Antrean loket kosong? false
Antrean piket kosong? trueKedua 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.
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;
}
}Rata-rata: 82.5
Nilai terbesar: 95Panggilan 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.
// tidak-dijalankan
Statistik.rataRata(nilai); // sah, dipanggil lewat nama interface
Statistik.maksimum(nilai); // sah juga
UjiStatistik.rataRata(nilai); // error: cannot find symbolKarena 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.
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";
}
}Laporan harianMenghapus override pada LaporanHarian membuat kompilasi berhenti dengan pesan yang menjelaskan konfliknya, sebagaimana diperlihatkan potongan berikut.
// tidak-dijalankan
class LaporanHarian implements Laporan, Arsip {
}
// error: class LaporanHarian inherits unrelated defaults for label()
// from types Laporan and ArsipAda 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 dengancannot find symbol. GunakanStatistik.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 badanlabel()sendiri membuat pemanggilan berputar tanpa henti sampaiStackOverflowError. Teruskan panggilan keNamaInterface.super.label()bila versi default tetap ingin dipakai.
Ringkasan#
- Metode
defaultmemberi 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
staticinterface 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#
- The Java Tutorials: Default Methods — Oracle — Penjelasan metode default, pewarisannya, dan skenario evolusi interface.
- JLS SE 21, Bab 9: Interfaces — Oracle — Spesifikasi metode default beserta aturan pewarisan dan resolusi konfliknya.
- Collection (Java SE 21 & JDK 21) — Oracle
— Contoh nyata interface yang mengembangkan kemampuan lewat metode default seperti
isEmpty()danstream().