Kod

Wyrażenia lambda i interfejsy funkcyjne w Javie

Wyrażenia lambda i interfejsy funkcyjne w Javie

Podstawy Pythona: Bezpłatny kurs dla wszystkich poziomów umiejętności Tworzenie portfolio: 4 angażujące projekty i interaktywna nauka Poznaj kurs: Czego możesz się spodziewać podczas kursu

Dowiedz się więcej

Utwórzmy funkcje, które obliczą sumę i iloczyn dwóch liczb:

Teraz utwórzmy pojedynczą metodę i nazwijmy ją przetwórzDwieLiczby. Ta metoda przyjmuje dwa parametry numeryczne i kod do ich przetworzenia.

private int processTwoNumbers(int a, int b, [tutaj wpisz kod])

Podczas korzystania z metody sum, trzeci parametr przyjmuje operację a+b jako argument, podczas gdy w przypadku metody mult, ten parametr przyjmuje wartość a*b.

Warto zauważyć, że trzeci argument może być tylko kodem, który przyjmuje dwa parametry określonego typu (w naszym przypadku int) i generuje wynik pożądanego typu — int.

Powinniśmy w jakiś sposób powiadomić kompilator, że jest to konieczne, aby zapobiec przekazywaniu przez przyszłych programistów nieprawidłowego kodu (na przykład a+b+c).

W tym przypadku potrzebujemy sygnatury metody, która będzie działać jako trzeci parametr dla naszej metody processTwoNumbers.

private int computeSumOfTwoIntegers(int a, int b, [sygnatura metody])

Jak Czy możemy dodać trzeci parametr bez nadmiernego komplikowania i utrudniania odczytu sygnatury metody processTwoNumbers? Twórcy Javy znaleźli eleganckie rozwiązanie tego problemu. Opracowali koncepcję interfejsów funkcyjnych.

Definicja interfejsu funkcyjnego: kluczowe aspekty

Interfejs funkcyjny to interfejs zawierający tylko jedną metodę abstrakcyjną, która jest opisem metody bez jej implementacji. W tym przypadku metody statyczne i metody z domyślną implementacją nie są brane pod uwagę, a ich liczba w interfejsie funkcyjnym jest nieograniczona.

Jeśli metoda przyjmuje interfejs funkcyjny jako parametr, to podczas jej wywołania należy przekazać blok kodu jako jeden z argumentów.

Dostarczony fragment kodu musi spełniać następujący wymóg: jego sygnatura musi być identyczna z sygnaturą jedynej metody abstrakcyjnej interfejsu funkcyjnego.

Aby wyjaśnić sytuację, przyjrzyjmy się konkretnemu przykładowi:

Java ma wiele predefiniowanych interfejsów funkcyjnych, które różnią się zarówno liczbą, jak i rodzajem parametrów wejściowych i wyjściowych. Przykładem takiego interfejsu jest wspomniany wcześniej interfejs ToIntBiFunction. Podczas tworzenia własnego interfejsu funkcyjnego należy pamiętać o dodaniu adnotacji @FunctionalInterface. Adnotacja ta gwarantuje, że kompilator będzie mógł zweryfikować, czy tworzony interfejs rzeczywiście spełnia kryteria interfejsu funkcyjnego.

Interfejs funkcyjny ToIntBiFunction idealnie pasuje do przykładu, od którego zaczęliśmy. Oznacza to, że możemy go użyć jako argumentu, przekazując kod, który:

  • Funkcja przyjmuje dwa argumenty (T t, U u), gdzie T i U oznaczają możliwość użycia parametrów różnych typów. Na przykład mogą to być Long i String. Jednak w naszym przypadku nie jest to konieczne, ponieważ oba argumenty mają ten sam typ — int.
  • Zwraca wynik jako liczbę całkowitą.

Oto, co otrzymasz:

Interfejs ToIntBiFunction oznacza, że ​​musisz podać metodę, która będzie miała taki sam podpis, jak metoda zdefiniowana w tym interfejsie.

Aby zaimplementować przekazany kod w metodzie processTwoNumbers, musisz wywołać tę metodę z interfejsu funkcyjnego.

Teraz przechodzimy do tematu wyrażeń lambda.

Zrozumienie wyrażeń lambda: podstawowe koncepcje

To zwięzła notacja, inspirowana rachunkiem lambda, służąca do przekazywania fragmentów kodu jako argumentów do innego programu.

Zasadniczo jest to klasa lub metoda bez nazwy. Ponieważ w Javie wszystko, poza typami prymitywnymi, jest obiektem, wyrażenia lambda muszą być również powiązane z określonym typem obiektu. Ten typ, jak można się domyślić, nazywa się interfejsem funkcyjnym.

Zatem wyrażenie lambda nie jest niezależnym elementem, lecz służy do implementacji metody zdefiniowanej w interfejsie funkcyjnym.

Gdyby nie istniały lambdy, za każdym razem, gdy wywoływalibyśmy metodę processTwoNumbers, musielibyśmy zastosować następujące podejście:

Należy zauważyć, że w podanym przykładzie biFunction jest implementowany za pomocą klas anonimowych. W przeciwnym razie musielibyśmy utworzyć osobną klasę implementującą interfejs ToIntBiFunction i zawierającą metodę applyAsInt. Dzięki użyciu klasy anonimowej mogliśmy wykonać to zadanie szybko i sprawnie bez tworzenia dodatkowych klas.

W podanym przykładzie cały kod oprócz jednej linii jest zbędny. Tylko wyrażenie return a + b przenosi obciążenie funkcjonalne, podczas gdy reszta kodu to nadmiar techniczny. Ich liczba okazała się dość znacząca, nawet w przypadku prostej implementacji operacji dodawania dwóch liczb.

W tym miejscu z pomocą przychodzą wyrażenia lambda. Dzięki nim proces tworzenia biFunkcji jest znacznie uproszczony i można go wykonać w zaledwie dziesięciu znakach!

Wyrażenie lambda ma następującą strukturę: (argumenty) -> (ciało funkcji)

Nasza lambda będzie wyglądać następująco:

Bifunkcję przyjmującą dwie liczby całkowite i zwracającą ich sumę można zdefiniować w następujący sposób: ToIntBiFunction biFunction = (a, b) -> a + b;

W ten sposób ten zestaw 10 znaków można wykorzystać jako argument metody przyjmującej interfejs funkcyjny jako parametr. W takim przypadku z reguły nie tworzą zmiennej pośredniej i przekazują lambdę bezpośrednio.

Kompilator upewni się, że wyrażenie lambda jest zgodne z wymaganiami interfejsu funkcjonalnego, co oznacza, że ​​akceptuje wymaganą liczbę argumentów wymaganych typów. W tym przypadku używamy interfejsu funkcjonalnego ToIntBiFunction. Jego jedyna abstrakcyjna metoda ma sygnaturę określającą dwa parametry: (Integer a, Integer b).

Na przykład, jeśli wywołasz tę metodę w ten sposób, kod nie skompiluje się, ponieważ użyty zostanie tylko jeden argument:

Istnieje wiele sposobów pisania wyrażeń lambda. Omówiliśmy tylko jeden z nich.

Używanie wyrażeń lambda w różnych obszarach.

W różnych sytuacjach. Jednym z typowych przykładów jest iteracja po elementach za pomocą pętli:

Wyrażenia lambda są również używane w komparatorach podczas implementacji sortowania. Rozważmy sytuację, w której musimy uporządkować kolekcję na podstawie ostatniej litery każdego słowa:

W większości przypadków, gdy używamy kolekcji w połączeniu z API Stream, trudno jest obejść się bez wyrażeń lambda. W poniższym przykładzie najpierw filtrujemy strumień danych na podstawie określonej wartości (używając metody filter), następnie przekształcamy każdy element (metoda map), a na koniec zbieramy wyniki do listy (metoda collect):