Wyślij zapytanie Dołącz do Sii

Wyrażenia lambda zostały dodane do Javy w wersji 8. Wprowadzenie tej funkcjonalności to krok w stronę programowania funkcyjnego w Javie.

Instrukcja jest blokiem kodu, który można przekazywać między częściami programu w celu późniejszego wykorzystania dowolną liczbę razy, bez przynależności do żadnej klasy. Lambda może być przekazywana jako obiekt i wywoływana, kiedy tego chcemy. Przyjmuje parametry i zwraca wartości. Możemy ją przypisywać do zmiennych.

Najważniejszymi zaletami wyrażeń lambda są:

  • Możliwość pisania kodu w sposób funkcyjny – pozwala to na pisanie bardziej zwięzłego kodu, czytelniejszego dla osoby, która go czyta.
  • Tworzenie mniejszej liczby klas.
  • Wyższa wydajność w pracy z kolekcjami.
  • Reużywalność wyrażeń.

Składnia wyrażenia lambda

Wyrażenie lambda ma postać:

Parametr -> wyrażenie

Parametrami są wymagane dane, a wyrażenie to ciało metody, czyli kod, który określa działanie wyrażenia. Możliwe jest wywołanie lambdy:

  • Bez parametrów:
() -> {wyrażenie}
Runnable lambdaWithoutParameters = () -> System.out.println("Lambda without parameters");
lambdaWithoutParameters.run();

Wydrukuje „Lambda without parameters”. Nie mamy tutaj żadnego parametru, a w wyrażeniu wypisujemy tylko wiadomość do konsoli.

  • Z jednym parametrem:
(parametr) -> {wyrażenie}
printable lambdaWithParametr = (String param) -> System.out.println("Lambda with " + param);
lambdaWithParametr.printSomething("parametr");

Wydrukuje „Lambda with parametr”. Przekazujemy parametr typu String i używamy go w wyrażeniu.

  • Z kilkoma parametrami:
(parametr, parametr) -> {wyrażenie}
BiConsumer lambdaWithParameters =
        (String param1, Integer param2) -> {
            for (int i = 0; i <= param2; i++) {
                System.out.println("Lambda with " + param1);
            }
        };
lambdaWithParameters.accept("parameters", 5);

Wydrukuje „Lambda with parameters” 5 razy. Przekazujemy do wyrażenia dwa parametry, String oraz Integer. Parametr pierwszy dodajemy do wypisywania w konsoli, a drugi oznacza ilość powtórzeń.

Przykład zastosowania wyrażenia lambda

Poniżej przedstawiam przykład użycia wyrażenia lambda do wypisania do konsoli wszystkich wartości zawartych w liście metodą forEach().

  • Tworzymy listę zawierającą kilka liczb całkowitych:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
  • Wypisujemy wszystkie liczby za pomocą lambdy:
numbers.forEach(n -> System.out.println(n));

Parametrem wyrażenia jest element listy oznaczony jako n. Wyrażeniem jest wywołanie wypisania do konsoli argumentu.

Interfejsy funkcyjne

Interfejs funkcyjny to interfejs zawierający tylko jedną metodę abstrakcyjną (może jednak zawierać wiele metod z klasy Object). Nazywane są również interfejsami SAM (Single Abstract Method Interface). Aby zaznaczyć, że interfejs ma być funkcyjny należy dodać do niego adnotacje @FunctionalInterface. Adnotacja nie jest obowiązkowa, ale zapewnia, że kompilator wyrzuci błąd na poziomie kompilacji, kiedy interfejs nie będzie spełniał warunków interfejsu funkcyjnego.

Przykład poprawnego interfejsu funkcyjnego:

@FunctionalInterface
interface printable {
    void printSomething(String msg);

    int hashCode();
    String toString();
    boolean equals(Object obj);
}

Wskazany wyżej interfejs posiada jedną metodę abstrakcyjną printSomething() oraz kilka metod z klasy Object.

Podstawowe kategorie interfejsów funkcyjnych

Do podstawowych kategorii interfejsów funkcyjnych należą poniższe:

  • Supplier
@FunctionalInterface
public interface Supplier<T> {
 
    T get();
}

Widzimy, że metoda nie przyjmuje żadnych parametrów i zwraca obiekt określonego przez nas typu.

Supplier<String> s = () -> "Hello world!";
System.out.println(s.get());

Powyższy przykład wydrukuje do konsoli „Hello world!”.

  • Consumer
@FunctionalInterface
public interface Consumer<T> {
 
    void accept(T t);
}

Interfejs przyjmuje argument i nie zwraca żadnego obiektu.

Consumer<String> print = x -> System.out.println(x);
print.accept("Hello world!");

Zastosowany przykład wydrukuje do konsoli „Hello world!”.

  • Predicate
@FunctionalInterface
public interface Predicate<T> {
 
    boolean test(T t);
      …
}

Interfejs przyjmuje parametr na wejściu i zwraca wartość logiczną prawda/fałsz na wyjściu.

Predicate<Integer> isOdd = number -> number % 2 != 0;
System.out.println(isOdd.test(3));

Wskazany przykład wydrukuje do konsoli „true”.

Metodę Predicate możemy łączyć za pomocą „and”:

Predicate<Integer> isOdd = number -> number % 2 == 0;
Predicate<Integer> isPositive = number -> number > 0;
System.out.println(isOdd.and(isPositive).test(4));

Najpierw sprawdzane jest, czy liczba jest parzysta, a później, czy jest większa od zera. Wynik to „true”.

  • Function
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
      …
}

Interfejs przyjmuje jeden argument i zwraca wartość po dokonaniu operacji.

Function<Integer, Double> multiplyByTen = value -> value * 10.0;
Double d = multiplyByTen.apply(5);

owyższy przykład przypisze do zmiennej „d” wartość „50.0”.

Możemy również łączyć interfejsy Function za pomocą „andThen”:

Function<Integer, Integer> addOne = value -> value + 1;
Function<Integer, Double> multiplyByTen = value -> value * 10.0;
System.out.println(addOne.andThen(multiplyByTen).apply(5));

W tym przypadku najpierw dodajemy 1 do naszej liczby, a następnie mnożymy wynik przez 10. W konsoli zostanie wyświetlone „60.0”.

Wzorce projektowe z użyciem lambd

Strategia

Wzorzec strategii pozwala nam na zmianę zachowania algorytmu w czasie wykonywania (runtime) oraz na wyeliminowanie instrukcji warunkowych w kodzie aplikacji. W pełnej implementacji wzorca tworzymy interfejs „Strategy”, który posiada tylko jedną metodę np. „execute” i jest wspólny dla wszystkich obsługiwanych algorytmów.

Następnie interfejs jest implementowany w poszczególnych klasach dostarczających konkretne algorytmy. We wzorcu wyróżniamy także klasę, która korzysta ze stworzonych algorytmów. Posiada ona referencje do aktualnie używanej strategii.

Zostało to zaprezentowane na poniższym wykresie.

Ryc. 1 Diagram obrazujący strategię
Ryc. 1 Diagram obrazujący strategię
public interface CountStrategy {
    Integer count(List<Integer> values);
}
 
public static class CountEvenValues implements CountStrategy {
 
    @Override
    public Integer count(List<Integer> values) {
        return values.stream()
                .filter(e -> e % 2 == 0)
                .reduce(0, Integer::sum);
    }
}
 
public static class CountOddValues implements CountStrategy {
 
    @Override
    public Integer count(List<Integer> values) {
        return values.stream()
                .filter(e -> e % 2 != 0)
                .reduce(0, Integer::sum);
    }
}
public static class Count {
    private final CountStrategy countStrategy;
 
    public Count(CountStrategy countStrategy) {
        this.countStrategy = countStrategy;
    }
 
    Integer count(List<Integer> values) {
        return countStrategy.count(values);
    }
}
public static class Main {
    public static void main(String[] args) {
        Count countEven = new Count(new CountEvenValues());
        Count countOdd = new Count(new CountOddValues());
 
        List<Integer> values = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
 
        System.out.println(countEven.count(values));
        System.out.println(countOdd.count(values));
    }
}

Powyższy przykład kodu wydrukuje do konsoli „30” i „25”.

Widzimy tutaj prosty przykład zastosowania wzorca strategii do obliczania sumy listy liczb w zależności od sposobu, w jaki chcemy je liczyć.

Podany przykład możemy zapisać łatwiej za pomocą interfejsu funkcyjnego Predicate:

public static int count(List<Integer> values, Predicate<Integer> condition) {
    return values.stream()
            .filter(condition)
            .reduce(0, Integer::sum);
}
public static void main(String[] args) {
    List<Integer> values = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    System.out.println(count(values, e -> e % 2 == 0));
    System.out.println(count(values, e -> e % 2 != 0));
}

W konsoli wyświetlą się wartości „30” i „25”.

Otrzymujemy więc ten sam wynik, używając mniejszej ilości kodu. Metoda „count” jest też w tym przypadku bardziej uniwersalna. Nie musimy do każdego warunku tworzyć osobnych klas, używamy tylko różnych warunków poprzez przekazany interfejs Predicate.

Dekorator

Wzorzec dekorator pozwala nam na dodawanie nowych obowiązków obiektom poprzez opakowywanie ich w specjalnych obiektach wrapujących, które definiują odpowiednie zachowania.

Zastosowanie możemy zobaczyć na poniższym wykresie.

Ryc. 2 Diagram obrazujący użycie dekoratora
Ryc. 2 Diagram obrazujący użycie dekoratora
public interface Book {
    String printBook();
}
 
public static class BasicBook implements Book {
 
    @Override
    public String printBook() {
        return "Basic Book";
    }
}
 
public static abstract class BookDecorator implements Book {
    private final Book book;
 
    public BookDecorator(Book book) {
        this.book = book;
    }
 
    @Override
    public String printBook() {
        return book.printBook();
    }
}
 
public static class FictionBookDecorator extends BookDecorator {
 
    public FictionBookDecorator(Book book) {
        super(book);
    }
 
    @Override
    public String printBook() {
        return "Fiction " + super.printBook();
    }
}
 
public static class ItBookDecorator extends BookDecorator {
 
    public ItBookDecorator(Book book) {
        super(book);
    }
 
    @Override
    public String printBook() {
        return "IT " + super.printBook();
    }
}
 
public static class SoftCoverBookDecorator extends BookDecorator {
 
    public SoftCoverBookDecorator(Book book) {
        super(book);
    }
 
    @Override
    public String printBook() {
        return super.printBook() + " with soft cover";
    }
}
 
public static class HardCoverBookDecorator extends BookDecorator {
 
    public HardCoverBookDecorator(Book book) {
        super(book);
    }
 
    @Override
    public String printBook() {
        return super.printBook() + " with hard cover ";
    }
}
 
public static void main(String[] args) {
    BasicBook book = new BasicBook();
 
    HardCoverBookDecorator hardCoverFictionBook = new HardCoverBookDecorator(new FictionBookDecorator(book));
    SoftCoverBookDecorator softCoverItBook = new SoftCoverBookDecorator(new ItBookDecorator(book));
 
    List<Book> books = Arrays.asList(hardCoverFictionBook, softCoverItBook);
 
    books.forEach(b -> System.out.println(b.printBook()));
}

Powyższy przykład wydrukuje do konsoli „Fiction Basic Book with hard cover” i „IT Basic Book with soft cover”.

Musimy tworzyć wiele klas, które rozbudują nasz bazowy obiekt. W przypadku, kiedy zagnieżdżamy więcej niż jednego dekoratora, aby stworzyć finalny obiekt, nie będzie to wyglądało czytelnie.

public interface Book {
    String printBook();
 
    static Book fictionBook(Book book) {
        return new Book() {
            @Override
            public String printBook() {
                return "Fiction " + book.printBook();
            }
        };
    }
 
    static Book withSoftCover(Book book) {
        return new Book() {
            @Override
            public String printBook() {
                return book.printBook() + " with soft cover";
            }
        };
    }
}
 
public static class BasicBook implements Book {
 
    @Override
    public String printBook() {
        return "Basic book";
    }
}
 
public static class BookDecorator {
    private final Function<Book, Book> attributes;
 
    private BookDecorator(Function<Book, Book>... desiredAttributes) {
        this.attributes = Stream.of(desiredAttributes)
                .reduce(Function.identity(), Function::andThen);
    }
 
    public static String printBook(Book book, Function<Book, Book>... desiredAttributes) {
        return new BookDecorator(desiredAttributes).printBook(book);
    }
 
    private String printBook(Book book) {
        return this.attributes.apply(book).printBook();
    }
}
 
public static void main(String[] args) {
    String finishedBook = BookDecorator.printBook(new BasicBook(), Book::fictionBook, Book::withSoftCover);
 
    System.out.println(finishedBook);
}

W tym przypadku w konsoli pojawi się napis „Fiction Basic book with soft cover”.
Przy użyciu lambd kod znacząco się skraca, a tworzenie skomplikowanych obiektów jest bardziej czytelne.

Podsumowanie

Wyrażenia lamdba zmieniły sposób programowania w języku Java. Pozwalają pisać krótszy kod, dają nam możliwość lepszej pracy z kolekcjami i strumieniami. Jak widać we wskazanych przykładach, możemy nawet zmieniać standardowe implementacje niektórych wzorców projektowych tak, aby były bardziej zwięzłe i czytelniejsze.

Jednakże, korzystanie z wyrażeń lambda ma również minusy:

  • Mogą wydawać się trudne dla początkujących programistów.
  • Trudniej je debugować niż zwykłe klasy i metody.
  • Ich testowanie jest trudniejsze.

Jak widać, plusy używania lambd przewyższają minusy. Ciężko wyobrazić sobie teraz powrót do czasów sprzed Javy 8 i wprowadzenia wyrażeń.

Źródła

***

A jeśli interesuje Cię wykorzystanie JavaScriptu w video konferencjach, zachęcamy do przeczytania artykułu naszego eksperta.

5/5 ( głosy: 9)
Ocena:
5/5 ( głosy: 9)
Autor
Avatar
Bartłomiej Pacocha

Software Developer w Sii, główne technologie z jakimi pracuje to Java, Spring i SQL. W wolnym czasie lubi czytać, odpręża się przy beletrystyce i amerykańskich komiksach.

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?