Skip to content

Wykład 1 - Wyrażenia Lambda i interfejsy funkcyjne

  • 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.
  • 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.
// bez argumentów
Runnable noArgs = () -> System.out.println("Hello");
// blok kodu (wiele instrukcji)
Runnable multi = () -> {
System.out.print("Hello");
System.out.println("World");
};
// wiele argumentów
BinaryOperator<Long> add = (x, y) -> x + y;
// jawne typy argumentów
BinaryOperator<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).
  • 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) -> boolean
Predicate<String> niePusty = s -> !s.isEmpty();
System.out.println(niePusty.test("abc")); // true
// Consumer<T>: accept(T) -> nic
Consumer<String> wypisz = s -> System.out.println("Wartosc: " + s);
wypisz.accept("hej"); // Wartosc: hej
// Supplier<T>: get() -> T
Supplier<Double> losowa = () -> Math.random();
System.out.println(losowa.get()); // np. 0.532...
// Function<T,R>: apply(T) -> R
Function<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) -> R
BiFunction<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)); // 5

Metody 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().

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.