Wyrażenia lambda i interfejsy funkcyjne zostały wprowadzone wraz z Javą 8. Ich celem było, by kod był krótszy i bardziej czytelny. Od Javy z numerem 8 minęło już kilka dobrych lat, a lambdy faktycznie zostały dobrze przyjęte i pracuję się z nimi na co dzień.
Nie zmienia to jednak faktu, że dla osób, które albo dopiero zaczynają swoją przygodę z Javą, albo są gdzieś na poziomie „advanced beginner” przejście z tradycyjnych form zapisu na lambdy może sprawić pewne trudności.
No i przede wszystkim warto dowiedzieć się, jak to wszystko działa pod maską.
Interfejsy funkcyjne
Podróż do dobrego opanowania wyrażeń lambda należy zacząć od interfejsów funkcyjnych. Definicja takiego interfejsu jest bardzo prosta: interfejsem funkcyjnym jest każdy interfejs, który posiada deklarację tylko i wyłącznie jednej metody abstrakcyjnej. Interfejs funkcyjny może posiadać metody domyślne (default) albo statyczne. Ważne jednak, aby posiadał tylko i wyłącznie jedną metodę abstrakcyjną.
Klasycznym przykładem takiego interfejsu jest interfejs Runnable
, który posiada deklarację tylko jednej metody: run
Na potrzeby tego wpisu zdefiniujemy własny interfejs funkcyjny
@FunctionalInterface public interface StringConnector { public String connect(String first, String second, String separator); }
Adnotacja @FunctionalInterface mówi kompilatorowi (i innym programistom), że dany interfejs ma być funkcyjny i nie należy tam dodawać kolejnych metod. Gdyby jednak tak się stało to kompilator wyrzuci błąd – Multiple non-overriding abstract methods found in interface StringConnector.
Interfejsy funkcyjne (i tylko takie) mogą zostać użyte w wyrażeniach lambda
Wyrażenia Lambda
Wyrażenia lambda powstały, aby skrócić zapis anonimowych klas wewnętrznych.
By użyć interfejsu StringConnector musimy albo napisać klasę go implementującą i definiującą dokładnie działanie metody connect, albo stworzyć anonimową klasę wewnętrzną:
public class LambTest { public static void main(String[] args) { StringConnector sc = new StringConnector() { @Override public String connect(String first, String second, String separator) { return first.concat(separator).concat(second); } }; String firstName = "Paweł"; String lastName = "Cwik"; System.out.println(sc.connect(firstName, lastName, " ")); } }
Taki zapis nie należy do najładniejszych. Za pomocą wyrażeń lambda można go skrócić, a przy okazji użyć metod znanych z programowania funkcyjnego w obiektowej Javie.
To teraz krok po kroku, wywalamy niepotrzebne „zapchajdziury”:
- Możemy usunąć z prawej strony nazwę interfejsu (mamy go z lewej), oraz zbędne nawiasy oraz adnotację @Override
StringConnector sc = public String connect(String first, String second, String separator) { return first.concat(separator).concat(second); }
2. Teraz w grę wchodzą właśnie interfejsy funkcyjne. Skoro StringConnector jest takim, znaczy to ma tylko jedną funkcję, więc nie musimy wprost podawać jej nazwy, bo skoro jest tylko jedna to kompilator ogarnie tę sytuację.
StringConnector sc = (String first, String second, String separator) { return first.concat(separator).concat(second); }
3. Podobnie ma się sytuacja z typami parametrów. Mamy tylko jedną metodę w interfejsie funkcyjnym, więc kompilator sobie poradzi
StringConnector sc = (first, second, separator) { return first.concat(separator).concat(second); }
4. I tak mogłoby zostać.. no ale postanowiono jeszcze dorzucić, dla czytelności zapewne, strzałkę
StringConnector sc = (first, second, separator) -> { return first.concat(separator).concat(second); }
5. Powyższa forma jest już w pełni legalną lambdą. Natomiast można to jeszcze skrócić. Jeśli całe ciało funkcji składa się tylko z jednej linii z return …, możemy pominąć nawiasy klamrowe oraz return.
public class LambTest { public static void main(String[] args) { StringConnector sc = (first, second, separator) -> first.concat(separator).concat(second); String firstName = "Paweł"; String lastName = "Cwik"; System.out.println(sc.connect(firstName, lastName, " ")); } }
Interfejsy funkcyjne wraz z wyrażeniami lambda znajdują zastosowanie w przeróżnych metodach. Najpopularniejsze z nich są te używane przy procesowaniu strumieni – filter, map czy collect.
names.stream().filter(String::isEmpty).map(empty->"Pusty").collect(Collectors.joining(","));
filter używa interfejsu funkcyjnego Predicate, map używa Function, a collect Collector.
By być na bieżąco i mieć realny wpływ na tematykę tworzonych przeze mnie artykułów i warsztatów zapraszam do dołączenia do mojego newslettera.
Bardzo dużo się od Ciebie nauczyłem, dzięki za pracę!