Wykład 1 - Wyrażenia Lambda i interfejsy funkcyjne
Pojęcia wstępne
Section titled “Pojęcia wstępne”- Boilerplate code - kod wymagany do działania programu, ale niewnoszący wartości; powtarzalny, “puchnie” (np. anonimowe klasy w Javie). Lambdy go redukują.
- Lukier składniowy (syntactic sugar) - cecha składni, którą da się usunąć prostym przekształceniem; istnieje tylko dla wygody i czytelności. Lambdy są lukrem składniowym dla klas anonimowych.
- Programowanie funkcyjne - paradygmat, w którym funkcje są jedynym i najważniejszym narzędziem; funkcje mogą przyjmować i zwracać inne funkcje. Styl deklaratywny: “co zrobić, a nie jak”.
- Rachunek lambda - system formalny (Alonzo Church + Kleene, 1930 r.), badał rekurencję, obliczalność, definiowalność funkcji. Programowanie obiektowe powstało dopiero w latach 60.
Czym jest lambda w Javie
Section titled “Czym jest lambda w Javie”- Lambda jest instancją interfejsu funkcyjnego.
- Interfejs funkcyjny = interfejs z dokładnie jedną metodą abstrakcyjną (SAM - Single Abstract Method).
- Metoda abstrakcyjna = metoda bez implementacji.
- Lambda to skrócona forma klasy anonimowej implementującej jedną metodę abstrakcyjną. To NIE jest pełne programowanie funkcyjne w Javie, lecz funkcyjny koncept zrealizowany obiektowo.
- Lambda = “kawałek kodu przekazywany do innego kodu w celu wykonania”; parametrem jest kod, nie obiekt/wartość.
- Kompilator sam wyznacza typy zmiennych lambdy z kontekstu (target typing), o ile nie ma niejednoznaczności.
Składnia i przykłady
Section titled “Składnia i przykłady”// bez argumentówRunnable noArgs = () -> System.out.println("Hello");
// blok kodu (wiele instrukcji)Runnable multi = () -> { System.out.print("Hello"); System.out.println("World");};
// wiele argumentówBinaryOperator<Long> add = (x, y) -> x + y;
// jawne typy argumentówBinaryOperator<Long> add2 = (long x, long y) -> x + y;- W lambdzie można używać zmiennych lokalnych, ale muszą być final lub efektywnie final (effectively final).
Adnotacja @FunctionalInterface
Section titled “Adnotacja @FunctionalInterface”- Wprowadzona w Javie 8.
- Oznacza interfejs jako funkcyjny i pozwala kompilatorowi sprawdzić, że ma dokładnie jedną metodę abstrakcyjną.
- UWAGA: interfejs z jedną metodą abstrakcyjną jest funkcyjny nawet bez adnotacji. Adnotacja tylko zabezpiecza przed pomyłkowym dodaniem drugiej metody (błąd kompilacji).
Wbudowane interfejsy funkcyjne (pakiet java.util.function)
Section titled “Wbudowane interfejsy funkcyjne (pakiet java.util.function)”Dodane w Javie 8 do unifikacji popularnych działań. Tabela do zapamiętania:
| Interfejs | Metoda abstrakcyjna | Wejście → Wyjście | Zastosowanie |
|---|---|---|---|
Predicate<T> |
boolean test(T t) |
T → boolean | filtrowanie, walidacja |
Consumer<T> |
void accept(T t) |
T → nic | efekty uboczne (log, zapis, wyświetlanie) |
Supplier<T> |
T get() |
nic → T | generowanie/dostarczanie danych |
Function<T,R> |
R apply(T t) |
T → R | transformacja danych |
UnaryOperator<T> |
T apply(T t) |
T → T | szczególny Function<T,T> |
BiFunction<T,U,R> |
R apply(T t, U u) |
(T,U) → R | operacje na dwóch wartościach |
BinaryOperator<T> |
T apply(T,T) |
(T,T) → T | szczególny BiFunction<T,T,T> |
Skróty mnemoniczne:
- Predicate = stwierdzenie prawda/fałsz.
- Consumer = bierze i nic nie oddaje.
- Supplier = nic nie bierze, zawsze coś daje (przeciwieństwo Consumera).
- Function = dostarcz coś → przetworzy → odda wynik (inny typ).
- UnaryOperator = dajesz coś, dostajesz coś tego samego typu.
- BinaryOperator = dwa argumenty tego samego typu, wynik tego samego typu.
Przykłady utworzenia i wywołania jeden po drugim:
// Predicate<T>: test(T) -> booleanPredicate<String> niePusty = s -> !s.isEmpty();System.out.println(niePusty.test("abc")); // true
// Consumer<T>: accept(T) -> nicConsumer<String> wypisz = s -> System.out.println("Wartosc: " + s);wypisz.accept("hej"); // Wartosc: hej
// Supplier<T>: get() -> TSupplier<Double> losowa = () -> Math.random();System.out.println(losowa.get()); // np. 0.532...
// Function<T,R>: apply(T) -> RFunction<String, Integer> dlugosc = s -> s.length();System.out.println(dlugosc.apply("kot")); // 3
// UnaryOperator<T>: apply(T) -> T (ten sam typ)UnaryOperator<String> naWielkie = s -> s.toUpperCase();System.out.println(naWielkie.apply("kot")); // KOT
// BiFunction<T,U,R>: apply(T,U) -> RBiFunction<Integer, Integer, String> opis = (a, b) -> "suma = " + (a + b);System.out.println(opis.apply(2, 3)); // suma = 5
// BinaryOperator<T>: apply(T,T) -> T (ten sam typ)BinaryOperator<Integer> dodaj = (a, b) -> a + b;System.out.println(dodaj.apply(2, 3)); // 5Metody statyczne i domyślne w interfejsach funkcyjnych
Section titled “Metody statyczne i domyślne w interfejsach funkcyjnych”Należą do interfejsu (nie do instancji), nie są implementowane przez klasy - działają jak metody pomocnicze (utility).
- Statyczne:
Predicate.isEqual(obj),Function.identity(),UnaryOperator.identity(),BinaryOperator.minBy(comp)/maxBy(comp). - Domyślne (default):
Predicate:and(),or(),negate();Function:andThen(),compose();Consumer:andThen().
Wstrzykiwanie logiki jako funkcji
Section titled “Wstrzykiwanie logiki jako funkcji”Logikę biznesową (walidację, obliczenia, akcję) można przekazać jako pola typu Predicate/UnaryOperator/Consumer i dynamicznie podmieniać zachowanie bez dziedziczenia.
class OrderProcessor { private Predicate<Order> validator; private UnaryOperator<Order> priceCalculator; private Consumer<Order> action; // process(): if (validator.test(o)) action.accept(priceCalculator.apply(o));}Do zapamiętania: lambda = instancja interfejsu funkcyjnego (1 metoda abstrakcyjna); skrót dla klasy anonimowej; @FunctionalInterface to tylko zabezpieczenie kompilacji; znać 7 wbudowanych interfejsów i ich sygnatury.