1. Wprowadzenie
Interfejs funkcyjny — to interfejs, który zawiera dokładnie jedną metodę abstrakcyjną (czyli metodę bez implementacji). Właśnie taki interfejs można wykorzystać do skróconego zapisu implementacji metody — w Javie robi się to za pomocą wyrażeń lambda (poznamy je później).
Dlaczego tylko jedna metoda?
Ponieważ interfejs funkcyjny opisuje dokładnie jedną operację. Gdyby metod było dwie lub więcej, stałoby się niejasne, którą z nich należy zaimplementować. Dlatego zasada jest prosta: jeden interfejs — jedna metoda abstrakcyjna.
Przykłady ze standardowej biblioteki
- Runnable — do zadań w wątkach (void run())
- Callable<V> — do zadań zwracających wynik (V call())
- Comparator<T> — do porównywania obiektów (int compare(T o1, T o2))
- Consumer<T> — „konsument” wartości (void accept(T t))
- Supplier<T> — „dostawca” wartości (T get())
- Function<T, R> — funkcja z T do R (R apply(T t))
- Predicate<T> — sprawdzenie warunku (boolean test(T t))
Oto na przykład interfejs Runnable:
public interface Runnable {
void run();
}
A oto interfejs Comparator:
public interface Comparator<T> {
int compare(T o1, T o2);
// ... są też metody default i static, ale metoda abstrakcyjna jest tylko jedna!
}
Ważna uwaga: metody default i static nie są liczone jako abstrakcyjne, więc może ich być dowolnie wiele!
2. Adnotacja @FunctionalInterface
Java to surowa i zasadnicza dama. Aby uniknąć nieporozumień, pozwala jawnie oznaczyć interfejs jako funkcyjny za pomocą adnotacji @FunctionalInterface. To jak naklejka „Działa tylko z jednym przyciskiem!” — żeby nikt nie dodał nic ponad to.
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
Teraz jeśli przypadkiem zapomnisz i dodasz drugą metodę abstrakcyjną, kompilator natychmiast zgłosi błąd:
@FunctionalInterface
public interface Oops {
void doIt();
void doSomethingElse(); // Błąd! Dwie metody abstrakcyjne
}
Czy adnotacja jest obowiązkowa?
Nie, nie jest to konieczne. Interfejs będzie funkcyjny i bez niej, jeśli zawiera dokładnie jedną metodę abstrakcyjną. Jednak dzięki adnotacji wyraźnie pokazujesz swoje zamiary i chronisz się przed przypadkowymi błędami.
Czy można dodawać metody default i static?
Tak, można! Najważniejsze, aby była tylko jedna metoda abstrakcyjna. Wszystkie pozostałe metody mogą być default lub static, ile tylko chcesz.
Przykład:
@FunctionalInterface
public interface FancyOperation {
int apply(int a, int b);
default void printInfo() {
System.out.println("Jestem fancy operacją!");
}
static void description() {
System.out.println("Interfejs funkcyjny do arytmetyki.");
}
}
3. Przykłady deklaracji i użycia
Załóżmy, że chcesz opisać operację na dwóch liczbach. Robi się to tak:
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
Teraz ten interfejs można zaimplementować na różne sposoby.
Implementacja za pomocą zwykłej klasy
public class SumOperation implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
Użycie:
Operation sum = new SumOperation();
System.out.println(sum.apply(2, 3)); // 5
Implementacja za pomocą klasy anonimowej
Operation multiply = new Operation() {
@Override
public int apply(int a, int b) {
return a * b;
}
};
System.out.println(multiply.apply(2, 3)); // 6
Uwaga o lambdach
Od Javy 8 takie interfejsy wygodnie implementuje się za pomocą wyrażeń lambda — to krótsza składnia. Lambdy omówimy za kilka wykładów, więc na razie wystarczy wiedzieć: interfejsy funkcyjne powstały właśnie po to, aby pracować z nimi jak najwygodniej.
4. Praktyka: napisz własny interfejs funkcyjny
Zadanie 1. Stwórz własny Action!
Utwórz interfejs Action, który przyjmuje napis i nic nie zwraca. Zaimplementuj go jako klasę anonimową, która wypisuje napis wielkimi literami.
@FunctionalInterface
interface Action {
void act(String s);
}
public class ActionDemo {
public static void main(String[] args) {
Action shout = new Action() {
@Override
public void act(String text) {
System.out.println(text.toUpperCase());
}
};
shout.act("uczę się java!"); // UCZĘ SIĘ JAVA!
}
}
(Później zobaczymy, jak zapisać to krócej za pomocą wyrażeń lambda.)
Zadanie 2. Filtr liczb
Utwórz interfejs NumberPredicate z metodą boolean test(int n). Zaimplementuj sprawdzenie parzystości za pomocą klasy anonimowej.
@FunctionalInterface
interface NumberPredicate {
boolean test(int n);
}
public class PredicateDemo {
public static void main(String[] args) {
NumberPredicate isEven = new NumberPredicate() {
@Override
public boolean test(int n) {
return n % 2 == 0;
}
};
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
}
}
Zadanie 3. Korzystamy ze standardowych interfejsów
Zamiast własnego interfejsu można użyć gotowego Predicate<Integer>:
import java.util.function.Predicate;
Predicate<Integer> isPositive = new Predicate<Integer>() {
@Override
public boolean test(Integer x) {
return x > 0;
}
};
System.out.println(isPositive.test(10)); // true
System.out.println(isPositive.test(-5)); // false
Tabela: interfejsy funkcyjne ze standardowej biblioteki
| Interfejs | Metoda | Opis | Przykład zastosowania |
|---|---|---|---|
|
|
Zadanie bez argumentów i wyniku | Wątki, timery |
|
|
Zadanie z wynikiem | Wątki, ExecutorService |
|
|
Porównywanie dwóch obiektów | Sortowanie kolekcji |
|
|
„Konsument” wartości | Iterowanie po kolekcji |
|
|
„Dostawca” wartości | Leniwa inicjalizacja, generowanie danych |
|
|
Funkcja z T do R | Przekształcanie danych |
|
|
Sprawdzenie warunku | Filtrowanie kolekcji |
5. Typowe błędy przy pracy z interfejsami funkcyjnymi
Błąd nr 1: dodano drugą metodę abstrakcyjną. Jeśli interfejs ma więcej niż jedną metodę abstrakcyjną, przestaje być funkcyjny. Kompilator (zwłaszcza z @FunctionalInterface) natychmiast zgłosi błąd.
Błąd nr 2: zapomniano, że metody default i static nie są liczone jako abstrakcyjne. Można śmiało dodawać je do interfejsu funkcyjnego — nie naruszy to zasady „jednej metody abstrakcyjnej”.
Błąd nr 3: błędnie zaimplementowano sygnaturę metody. Na przykład interfejs wymaga dwóch argumentów, a napisano metodę tylko z jednym. Zawsze sprawdzaj sygnatury.
Błąd nr 4: brak użycia @FunctionalInterface i przypadkowe zepsucie interfejsu. Jeśli nie oznaczysz interfejsu adnotacją, możesz przypadkowo dodać zbędną metodę — a potem długo szukać, dlaczego kod nie działa. Lepiej zawsze dodać tę adnotację dla jasności.
GO TO FULL VERSION