Setiap lambda expression di Java selalu punya rumah: sebuah functional interface. Konsep ini adalah fondasi seluruh gaya fungsional di Java — tanpa memahaminya, lambda akan terasa seperti sintaks yang melayang tanpa tempat berlabuh. Kabar baiknya, konsepnya singkat: functional interface hanyalah interface dengan tepat satu method abstrak. Pada Chapter ini kita akan membedah anatomi interface semacam itu, membuat versi buatan sendiri, dan melihat bagaimana anotasi @FunctionalInterface melindungi kontraknya dari kesalahan tak sengaja.

Kontrak Single Abstract Method #

Functional interface didefinisikan oleh aturan Single Abstract Method (SAM): interface harus memiliki tepat satu method abstrak — method tanpa tubuh yang wajib diimplementasikan. Method default, method static, dan peng-override method dari Object seperti toString() tidak dihitung; yang dihitung hanya method abstrak “asli”. Contoh SAM yang sudah lama ada di Java: Runnable (method abstrak run()), Comparator<T> (method abstrak compare()), dan Callable<V> (method abstrak call()).

Aturan SAM menjadi penting karena cara compiler javac menerjemahkan lambda: sebuah lambda adalah implementasi dari method abstrak tunggal itu. Bentuk lambda (harga) -> harga * 0.9 tidak berdiri sendiri — compiler membaca tipe variabel atau parameter di sekitarnya (disebut target type), menemukan method abstrak tunggalnya, lalu memperlakukan lambda sebagai tubuh method tersebut. Satu method abstrak berarti satu hubungan tegas antara lambda dan kontraknya; dua method abstrak akan membuat kompilator kebingungan method mana yang harus diisi.

Membuat Functional Interface Sendiri #

Kita tidak terpaku pada interface bawaan — membuat sendiri pun sah, bahkan berguna ketika domain bisnis kita butuh istilah yang jelas. Berikut interface PemberiDiskon dengan satu method abstrak hitung, dilengkapi method default dan static:

java DiskonToko.java
@FunctionalInterface
interface PemberiDiskon {
    double hitung(double harga);

    // method default dan static tidak membatalkan status SAM
    default double setelahPajak(double harga) {
        return hitung(harga) * 1.11;
    }

    static PemberiDiskon tetap(double persen) {
        return harga -> harga * (1 - persen / 100);
    }
}

public class DiskonToko {
    public static void main(String[] args) {
        PemberiDiskon diskonAnggota = harga -> harga * 0.9;
        PemberiDiskon diskonAkhirTahun = harga -> harga - 20000;

        System.out.printf("Anggota        : %.2f%n", diskonAnggota.hitung(150000));
        System.out.printf("Akhir tahun    : %.2f%n", diskonAkhirTahun.hitung(150000));
        System.out.printf("Anggota+pajak  : %.2f%n", diskonAnggota.setelahPajak(150000));
        System.out.printf("Dari factory   : %.2f%n", PemberiDiskon.tetap(25).hitung(80000));
    }
}

Output yang Dihasilkan:

text
Anggota        : 135000.00
Akhir tahun    : 130000.00
Anggota+pajak  : 149850.00
Dari factory   : 60000.00

Mari kita bedah berkas tersebut baris demi baris:

  1. Baris 1 (@FunctionalInterface): anotasi penanda bahwa interface ini dimaksudkan sebagai SAM. Anotasi ini bersifat dokumentasi sekaligus pengaman — perilakunya kita uji sebentar lagi.
  2. Baris 2-3 (interface PemberiDiskon { double hitung(...) }): method hitung adalah satu-satunya method abstrak. Ia menerima harga dan mengembalikan nilai diskon — itulah kontraknya.
  3. Baris 6-8 (default double setelahPajak(...)): method default punya tubuh sendiri sehingga tidak dihitung sebagai method abstrak kedua. Perhatikan bahwa tubuhnya boleh memanggil hitung(harga) — method abstrak yang diimplementasikan lambda di runtime.
  4. Baris 10-12 (static PemberiDiskon tetap(...)): method static juga tidak membatalkan status SAM. Method ini berperan sebagai factory yang mengembalikan lambda — bukti bahwa lambda bisa diproduksi dan dikembalikan seperti nilai biasa.
  5. Baris 18-19 (PemberiDiskon diskonAnggota = harga -> harga * 0.9;): dua variabel bertipe sama, dua perilaku berbeda — inilah behavior sebagai nilai. Compiler tahu lambda mengisi hitung(double) karena PemberiDiskon hanya punya satu kandidat.
  6. Baris 21-24: setelahPajak dijalankan tanpa implementasi tambahan (tubuh default dipakai), dan PemberiDiskon.tetap(25) membuat instance baru lewat factory static.

Ketika Kontrak Dilanggar #

Anotasi @FunctionalInterface bukan dekorasi. Saat seseorang — mungkin kita sendiri di masa depan — menambahkan method abstrak kedua, compiler langsung menolak kompilasi. Program berikut sengaja melanggar aturan SAM:

@FunctionalInterface
interface Verifikasi {
    boolean periksa(String teks);

    // Method abstrak kedua merusak status single abstract method
    void laporkan(String teks);
}

public class KontrakGanda {
    public static void main(String[] args) {
        Verifikasi v = teks -> teks.isEmpty();
        System.out.println(v.periksa("halo"));
    }
}

Berkas tersebut gagal dikompilasi dengan dua pesan yang saling terkait. Pesan pertama menyasar anotasinya:

text
KontrakGanda.java:1: error: Unexpected @FunctionalInterface annotation
  Verifikasi is not a functional interface
    multiple non-overriding abstract methods found in interface Verifikasi

Pesan kedua menyasar pemakaian lambdanya — incompatible types: Verifikasi is not a functional interface — karena dua method abstrak membuat compiler tidak bisa memutuskan method mana yang diisi teks -> teks.isEmpty(). Tanpa anotasi, pesan kedua inilah satu-satunya petunjuk; dengan anotasi, kesalahan tertangkap lebih awal dan alasannya ditampilkan gamblang. Itulah alasan kita membudayakan @FunctionalInterface pada setiap interface yang dimaksudkan menerima lambda.

Satu Kontrak, Banyak Asal #

Functional interface tidak peduli asal-usulnya: bisa buatan sendiri seperti PemberiDiskon, bisa bawaan platform seperti Runnable, bisa pula dari paket java.util.function seperti Predicate<T>. Selama aturan SAM terpenuhi, lambda, method reference, anonymous class, dan class implementasi biasa semuanya sah sebagai implementasi. Kebebasan inilah yang membuat pustaka lama (misalnya Thread(Runnable)) langsung bisa menikmati lambda tanpa perubahan API.

Kita sudah memahami rumahnya; sekarang saatnya menghuninya dengan lebih cermat. Chapter berikutnya membahas lambda expression secara penuh: empat bentuk penulisannya, aturan inferensi tipe, dan kapan lambda menggantikan anonymous class dengan rapi.

Referensi #

Berkas Sumber

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

DiskonToko.java

Unduh berkas
java
@FunctionalInterface
interface PemberiDiskon {
    double hitung(double harga);

    // method default dan static tidak membatalkan status SAM
    default double setelahPajak(double harga) {
        return hitung(harga) * 1.11;
    }

    static PemberiDiskon tetap(double persen) {
        return harga -> harga * (1 - persen / 100);
    }
}

public class DiskonToko {
    public static void main(String[] args) {
        PemberiDiskon diskonAnggota = harga -> harga * 0.9;
        PemberiDiskon diskonAkhirTahun = harga -> harga - 20000;

        System.out.printf("Anggota        : %.2f%n", diskonAnggota.hitung(150000));
        System.out.printf("Akhir tahun    : %.2f%n", diskonAkhirTahun.hitung(150000));
        System.out.printf("Anggota+pajak  : %.2f%n", diskonAnggota.setelahPajak(150000));
        System.out.printf("Dari factory   : %.2f%n", PemberiDiskon.tetap(25).hitung(80000));
    }
}

KontrakGanda.java

Unduh berkas
java
@FunctionalInterface
interface Verifikasi {
    boolean periksa(String teks);

    // Method abstrak kedua merusak status single abstract method
    void laporkan(String teks);
}

public class KontrakGanda {
    public static void main(String[] args) {
        Verifikasi v = teks -> teks.isEmpty();
        System.out.println(v.periksa("halo"));
    }
}