Wyrażenia lambda i interfejsy funkcyjne

Wyrażenia lambda i interfejsy funkcyjne zostały wprowadzone wraz z Javą 8. Zostały dodane, aby ułatwić życie programistom i zachęcić ich do tworzenia kodu funkcyjnego, a nie imperatywnego. Dzięki ich właściwemu użyciu kod staje się krótszy, bardziej czytelny i przejrzysty. Z perspektywy lat (mijają już ponad 4 lata od wydania Javy 8) można stwierdzić, że wyrażenia lambda oraz interfejsy funkcyjne zostały dobrze przyjęte w środowisku: są używane chętnie i często.

Interfejsy funkcyjne

Interfejsy funkcyjne zostały wprowadzone w Javie 8, aby umożliwić działanie funkcyjne w wyrażeniach lambda.

Definicja takiego interfejsu jest bardzo prosta: interfejsem funkcyjnym jest każdy interfejs, który posiada deklarację tylko i wyłącznie jednej metody abstrakcyjnej. Dla przypomnienia: metoda abstrakcyjna to taka metoda, która nie posiada ciała, czyli definicji. Interfejs funkcyjny może posiadać metody defaultowe albo statyczne. Ważne jednak, aby posiadał tylko i wyłącznie jedną metodę abstrakcyjną.

Klasycznym przykładem takiego interfejsu, znanym jeszcze sprzed Javy 8, jest interfejs Runnable, który posiada deklarację tylko jednej metody: run.

UWAGA: Specjalnym wyjątkiem są metody, które nadpisują metody abstrakcyjne z klasy java.lang.Object takie jak np. equals. Obecność takich metod jest dozwolona w interfejsach funkcyjnych. Za przykład takiego wyjątku może posłużyć opisywany niedawno interfejs Comparator. Jest on interfejsem funkcyjnym, mimo że posiada nie tylko deklarację metody abstrakcyjnej compareTo, ale również deklarację metody equals.

Aby lepiej określić intencję programistyczną, dobrze jest oznaczyć dany interfejs jako funkcyjny za pomocą adnotacji @FunctionalInterface. Taka adnotacja nie spełnia jednak jedynie funkcji dekoracyjno-informującej. Jeśli zostanie umieszczona nad interfejsem, w którym nie znajduje się dokładnie jedna metoda abstrakcyjna, to wystąpi błąd kompilacji.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Gotowe vs customowe

Wraz z nadejściem konceptu interfejsów funkcyjnych, pojawiła się również nowa paczka: java.function, w której znajdziemy całe mnóstwo „gotowych” interfejsów funkcyjnych, z których możemy skorzystać w wyrażeniach lambda (o których za chwilę). Pojawiają się tutaj interfejsy, których nazewnictwo ma nam podpowiedzieć jakiego rodzaju metoda znajduje się w tym interfejsie. Jest to na przykład:

  • Consumer<T>, który posiada metodę accept(T t). Tu więc sprawa jest dość oczywista: jest to interfejs z metodą, która „konsumuje” dany przekazany argument, coś z nim robiąc, ale nie zwraca niczego. Najprostszym przykładem wykorzystania implementacji takiego interfejsu jest chociażby System.out.println. Nic nie zwraca, za to przyjmuje jeden argument – String i wypisuje go na standardowym wyjściu,
  • Supplier<T> – przeciwieństwo Consumera. Nie przyjmuje żadnych argumentów, za to zwraca wartość danego typu za pomocą metody get(). Przykład Suppliera: new Random().nextInt().

Tych „gotowych” interfejsów jest mnóstwo. Cała lista jest dostępna na stronie Oracle: TUTAJ.

Jeśli jednak chcemy poćwiczyć lub też na powyższej liście nie znajdziemy interfejsu, który będzie odpowiadał naszym potrzebom, to można się również pokusić o napisanie własnego interfejsu funkcyjnego. Na przykład:

@FunctionalInterface
public interface Sum {

    int calculate(int a, int b);

}

Oczywiście sam interfejs nie wystarczy. Potrzebna jest również jego implementacja. I tutaj ponownie mamy wybór: możemy sami napisać klasę implementującą albo skorzystać z dostępnego rozwiązania.

Jak jednak wykorzystać taki interfejs funkcjonalny? Oczywiście w wyrażeniach lambda!

Wyrażenia lambda

Wyrażenia lambda zostały wprowadzone, aby skrócić zapis anonimowych klas wewnętrznych.

Weźmy nasz interfejs Sum. Aby użyć go w danym miejscu możemy albo napisać klasę implementującą metodę calculate, albo utworzyć w wybranym miejscu anonimową klasę wewnętrzną:

int x = 2;
int y = 5;

Sum sum = new Sum() {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
};


System.out.println(sum.calculate(2, 5));

Taki zapis nie podobał się jednak wielu osobom, więc postanowiono go zmienić, przy okazji promując bardziej funkcyjne podejście do programowania w Javie.

Na skróty

Skoro mamy już przed znakiem równości podaną nazwę interfejsu, to po co dublować kod po prawej stronie? Usuwamy więc nazwę i nawiasy klamrowe, które tylko niepotrzebnie zajmują miejsce:

Sum sum =
    @Override
    public int calculate(int a, int b) {
        return a + b;
    };

To jednak nie koniec: możemy usunąć też adnotację @Overrride i nazwę metody calculate. I po raz kolejny usuwamy nikomu niepotrzebne nawiasy klamrowe:

Sum sum = (int a, int b) return a + b;

Zostały nam tylko nawiasy okrągłe z deklaracją parametrów i zwracana suma. I to byłoby prawie na tyle, jednak nie musimy podawać typu argumentów, Java się tego domyśli. Równie niepotrzebne jest słowo kluczowe return. Co zatem nam zostanie?

Sum sum = (a, b)  a + b;

I na sam koniec zostało nam jeszcze dodanie takiej symbolicznej „kropki nad i” w postaci strzałki charakterystycznej dla wyrażeń lambda:

Sum sum = (a, b) -> a + b;

I teraz to już wszystko! Ale jak to zapamiętać i się nie pomylić?

Ten skrót działa w bardzo prosty sposób. W interfejsie funkcyjnym mamy deklarację metody abstrakcyjnej, która będzie coś zwracać albo nie. Będzie ona też przyjmować jakieś parametry albo nie.

I analogicznie w naszym zapisie lambda. Można to przedstawić w ten sposób:

(lista parametrów metody) -> ciało metody

Proste, prawda? Parametry, strzałka i ciało lub wynik metody. Jest jeszcze jeden skrót (już ostatni – obiecuję!): jeśli dana metoda abstrakcyjna ma tylko jeden parametr, to możemy się pozbyć również nawiasów okrągłych.

Załóżmy, że mamy taki interfejs:

@FunctionalInterface
public interface Factor {
    
    int calculate(int a);
    
}

Wtedy przykładowy zapis lambda wyglądałby tak:

Factor factor = a -> a * a;

Jest to analogiczne odbicie tego, co chcielibyśmy zdefiniować w anonimowej klasie wewnętrznej, tylko o wiele prostsze.

Interfejsy funkcyjne wraz z wyrażeniami lambda znajdują zastosowanie w przeróżnych metodach. Prostym przykładem jednej z nich jest lista elementów, na której możemy wywołać metodę forEach. Przyjmuje ona jako typ wspomniany wyżej interfejs funkcyjny Consumer:

foreach dev foundry blog programowanie java spring kursy
Metoda forEach przyjmująca interfejs funkcyjny Consumer jako parametr

W tym wypadku możemy zatem użyć znanego nam już Consumera, czyli System.out.println:

integerList.forEach(element -> System.out.println(element));

Wyrażenia lambda mogą również mieć więcej niż jedną linię kodu. Wtedy nie będziemy już w stanie uniknąć nawiasów klamrowych:

integerList.forEach(element -> {
    int x = 5;
    System.out.println("Element: " + element * x);
});

Oczywiście przyzwyczajenie się do takiego zapisu zajmuje trochę czasu, ale serdecznie polecam, bo jest to coś, co dobrze opanowane potrafi zaoszczędzić programiście sporą ilość czasu.

Referencje metod

Referencje metod są kolejnym ułatwieniem, z którego możemy skorzystać, aby zyskać na czasie i czytelności naszego kodu.

Weźmy za przykład interfejs funkcyjny, który ma deklarację metody, która nic nie zwraca i nie przyjmuje żadnych parametrów:

@FunctionalInterface
public interface NothingSpecial {

    void nothing();

}

Teraz przy implementacji tego interfejsu chcielibyśmy wywołać jakąś inną metodę, na przykład taką, która wyświetli prostą wiadomość na ekranie. Korzystając z poznanego już zapisu lambda, piszemy:

public static void main(String[] args) {

    NothingSpecial nothingSpecial = () -> printMessage();

    nothingSpecial.nothing();

}

private static void printMessage() {
    System.out.println("Hello");
}

Zapis jest zatem dość prosty:

  • ( ) puste nawiasy, które jednak muszą się pojawić w sytuacji, gdy metoda interfejsu nie przyjmuje parametrów,
  • -> strzałka,
  • ciało metody, w tym wypadku wywołanie innej metody.

Wciąż są to zatem trzy elementy, które już znamy. A teraz skrót: Jeśli nasz zapis lambda działa tylko jako pośrednik do wywołania innej metody, to zamiast takiego zapisu:

() -> printMessage();

Możemy zapisać:

TestApp::printMessage

Taka przykładowa konstrukcja jest prosta:

nazwa klasy lub obiektu::nazwa metody

Znamienny jest tutaj podwójny dwukropek.

UWAGA: Jedynym wymaganiem jest to, żeby parametry metody abstrakcyjnej interfejsu funkcyjnego i metody w wyrażeniu lambda były takie same (tego samego typu, kolejności i ilości).

Użyjmy teraz referencji metody na powyższym przykładzie listy Integerów, w metodzie forEach. Zapis będzie wyglądał podobnie do tego powyżej:

integerList.forEach(System.out::println);

Istnieją 4 typy referencji metod. I tutaj pozwolę sobie odwołać się do oficjalnej dokumentacji na stronie Oracle, gdzie znajdziemy poniższą tabelę:

Kind Example
Reference to a static method ContainingClass::staticMethodName
Reference to an instance method of a particular object containingObject::instanceMethodName
Reference to an instance method of an arbitrary object of a particular type ContainingType::methodName
Reference to a constructor ClassName::new

Tu zapewne znowu potrzeba będzie trochę czasu i praktyki, aby w pełni przyzwyczaić się do takiego zapisu – ale myślę, że warto.

Podsumowanie

Poznawanie wyrażeń lambda i interfejsów funkcyjnych może być czasochłonne, a na początku również i frustrujące. Na początku trudne może się okazać również zrozumienie jakiegoś dłuższego, wielolinijkowego kodu wyrażenia lambda, którego autorem są nasze koleżanki i koledzy z zespołu. Jednak praktyka i cierpliwość z pewnością zostaną wynagrodzone.

Interfejsy funkcyjne i wyrażenia lambda oraz referencje metod są tutaj po to, aby ułatwić nam życie. Uczmy się więc ich i korzystajmy z nich mądrze, a nasz kod będzie bardziej czytelny, a nasza wydajność programistyczna wzrośnie.

Kod z przykładami do powyższego wpisu znajdziesz na githubie.


Podziel się tym wpisem:

5 komentarzy do wpisu „Wyrażenia lambda i interfejsy funkcyjne

  1. Bardzo ciekawy artykuł! naprawdę pozwolił mi dogłębnie zrozumieć zasadę działania Lambd. Często ich używam i znam ich zasadę działania ale nie sądziłem, że tak wygląda ich geneza 🙂 Dzięki za materiał

Dodaj komentarz