Interface Fungsional

Mengenal Runnable, Comparator, dan keluarga java.util.function sebagai fondasi lambda.

Daftar isi

Interface fungsional (functional interface) adalah interface yang memuat tepat satu metode abstrak, sehingga isi kontraknya dapat dinyatakan oleh satu fungsi saja. Bentuk inilah fondasi lambda di Java, karena ekspresi lambda memang diciptakan untuk mengisi metode tunggal tersebut dengan ringkas. Topik ini mengenalkan kontraknya melalui Runnable dan Comparator, menelusuri keluarga java.util.function, lalu menunjukkan bentuk lambda yang dituju oleh semua kontrak tersebut.

Satu metode abstrak sebagai kontrak#

Yang menentukan sebuah interface fungsional atau bukan adalah jumlah metode abstraknya, bukan jumlah anggotanya. Metode default dan static yang membawa isi tidak ikut dihitung, sehingga interface dengan banyak metode konkret tetap fungsional selama metode abstraknya hanya satu. Anotasi @FunctionalInterface boleh dipasang pada deklarasinya, dan anotasi itu meminta kompiler memeriksa syaratnya secara eksplisit, kendati tanpa anotasi pun interface tetap fungsional bila syaratnya terpenuhi.

java Contoh: anotasi @FunctionalInterface pada kontrak tunggal
// tidak-dijalankan
@FunctionalInterface
interface KonverterSuhu {
    double konversi(double celsius);
}

Bila kontraknya melanggar syarat, misalnya memuat dua metode abstrak, kompiler menolaknya dengan pesan Unexpected @FunctionalInterface annotation beserta alasan multiple non-overriding abstract methods found. Pemeriksaan ini menjaga niat desain tetap tercatat, yaitu interface itu memang diperuntukkan sebagai tuan rumah sebuah fungsi.

Runnable dan Comparator#

Runnable adalah interface fungsional yang mewakili tugas tanpa masukan dan tanpa nilai kembalian, dengan satu metode abstrak bernama run. Interface ini lazim diteruskan ke thread, tetapi pada contoh berikut isinya dipanggil langsung agar keluarannya mudah diamati. Isi metode tunggal itu diberikan lewat anonymous class, yaitu kelas tanpa nama yang dideklarasikan langsung di tempat ekspresinya ditulis.

java DemoRunnable.java
public class DemoRunnable {

    public static void main(String[] args) {
        Runnable tugas = new Runnable() {
            public void run() {
                System.out.println("Laporan harian selesai dibuat.");
            }
        };

        tugas.run();
        tugas.run();
    }
}
text Keluaran
Laporan harian selesai dibuat.
Laporan harian selesai dibuat.

Comparator<T> adalah interface fungsional yang mewakili pembanding dua objek, dengan metode abstrak compare(T a, T b) yang mengembalikan bilangan negatif, nol, atau positif sesuai urutan yang dikehendaki. Pustaka pengurutan menerima pembanding ini sebagai argumen, sehingga aturan urutan dapat ditentukan pemakai tanpa mengubah kelas yang dibandingkan.

java UjiComparator.java
import java.util.Arrays;
import java.util.Comparator;

public class UjiComparator {

    public static void main(String[] args) {
        String[] kata = {"semangka", "api", "ombak", "rumah"};

        Arrays.sort(kata, new Comparator<String>() {
            public int compare(String a, String b) {
                return Integer.compare(a.length(), b.length());
            }
        });

        for (String k : kata) {
            System.out.println(k);
        }
    }
}
text Keluaran
api
ombak
rumah
semangka

Pengurutan di atas memilah kata menurut panjangnya, bukan menurut abjadnya, dan penentuannya seluruhnya berada di dalam compare. Dua contoh ini memperlihatkan pola yang sama, yaitu perilaku dikemas sebagai objek karena interfacenya hanya menuntut satu metode.

Keluarga java.util.function#

Paket java.util.function menghimpun interface fungsional generik yang menutupi bentuk-bentuk fungsi yang paling sering dibutuhkan, sehingga kontrak serupa tidak perlu ditulis ulang untuk tiap proyek. Empat anggotanya paling sering muncul dan layak dihafalkan bentuknya.

InterfaceMetode abstrakPeran
Function<T,R>R apply(T t)mengubah masukan menjadi keluaran
Predicate<T>boolean test(T t)menguji syarat dan menjawab boolean
Consumer<T>void accept(T t)memakai nilai tanpa mengembalikan apa pun
Supplier<T>T get()menyediakan nilai tanpa masukan

Program berikut memakai Function dan Predicate dari paket tersebut, lengkap dengan isi metodenya yang ditulis lewat anonymous class seperti pada contoh sebelumnya.

java UjiFunction.java
import java.util.function.Function;
import java.util.function.Predicate;

public class UjiFunction {

    public static void main(String[] args) {
        Function<String, Integer> panjang = new Function<>() {
            public Integer apply(String teks) {
                return teks.length();
            }
        };
        Predicate<Integer> genap = new Predicate<>() {
            public boolean test(Integer angka) {
                return angka % 2 == 0;
            }
        };

        String kata = "antrean";
        System.out.println("Panjang \"" + kata + "\": " + panjang.apply(kata));
        System.out.println("12 genap? " + genap.test(12));
        System.out.println("13 genap? " + genap.test(13));
    }
}
text Keluaran
Panjang "antrean": 7
12 genap? true
13 genap? false

Function menerima teks dan mengembalikan panjangnya sebagai Integer, sedangkan Predicate menerima bilangan dan menjawab apakah nilainya genap. Kontrak serupa di pustaka standar, misalnya filter pada collections, menerima Predicate untuk syaratnya dan Function untuk pengubahannya, sehingga keempat interface di tabel itu akan terus ditemui.

Jembatan menuju lambda#

Penulisan anonymous class pada contoh di atas masih menyertakan tanda kurung, kata kunci, dan tanda tangan metode, padahal yang penting hanyalah isinya. Sintaks lambda menyederhanakan penulisan itu menjadi daftar parameter diikuti panah dan isi, dan pemetaannya persis pada satu metode abstrak yang dijanjikan interface fungsionalnya.

java Contoh: lambda mengisi metode abstrak tunggal
// tidak-dijalankan
Runnable tugas = () -> System.out.println("Laporan harian selesai dibuat.");
Comparator<String> menurutPanjang = (a, b) -> Integer.compare(a.length(), b.length());

Bentuk () -> ... dan (a, b) -> ... itu dipelajari secara penuh pada topik Lambda, tetapi tujuannya sudah terlihat dari sini, yaitu menggantikan kerangka anonymous class dengan isi metodenya saja. Semua yang ditulis new Runnable() { ... } di topik ini dapat ditulis ulang dalam satu baris lambda, dan keduanya memenuhi kontrak yang sama persis.

Kesalahan umum#

  • Menambah metode abstrak kedua pada interface fungsional. Kontraknya kehilangan syarat lambda, dan bila @FunctionalInterface terpasang, kompiler menolaknya dengan pesan multiple non-overriding abstract methods found. Pindahkan kemampuan tambahan itu ke interface lain.
  • Menganggap metode default mengubah kefungsionalan. Metode dengan isi tidak dihitung, sehingga penghapusan atau penambahan metode default tidak pernah membuat interface berhenti fungsional. Yang patut dijaga hanyalah jumlah metode abstraknya.
  • Melupakan public pada implementasi di anonymous class. Metode interface implisit publik, sehingga penulisan void run() tanpa public di dalam kelas tanpa nama ditolak kompiler. Sertakan public sebagaimana pada penerapan interface biasa.
  • Mengira setiap interface dengan satu metode otomatis menerima lambda. Syaratnya satu metode abstrak, bukan satu metode apa pun, dan kompiler menilai dari seluruh anggota yang diwarisi. Interface yang mewarisi metode abstrak dari induknya pun tetap dihitung jumlah abstraknya.

Ringkasan#

  • Interface fungsional memuat tepat satu metode abstrak, dengan metode default dan static tidak ikut dihitung.
  • Runnable mewakili tugas tanpa masukan dan keluaran, sedangkan Comparator<T> mewakili pembanding dua objek, dan keduanya menerima isi lewat anonymous class maupun lambda.
  • Paket java.util.function menyediakan kontrak generik yang paling sering dipakai, yaitu Function, Predicate, Consumer, dan Supplier.
  • Anotasi @FunctionalInterface bersifat opsional dan tugasnya meminta kompiler menjaga syarat satu metode abstrak itu tetap terpenuhi.

Referensi#