W tym wpisie zajmiemy się takimi interfejsami jak Consumer, Function, UnaryOperator. <!– end –> Oczywiście są odmianami interfejsów opisanych w tym wpisie. Różnica między nimi jest taka, że działają na pojedynczych argumentach. Praktyki nigdy nie za mało, więc zaczynajmy!

Consumer

Przed nami interfejs funkcjonalny Consumer<T>, jego zadanie jest proste. Przyjąć argument, wykonać operację i zakończyć pracę. Zobaczcie sami, jak wygląda:

@FunctionalInterface
interface Consumer<T> {
​
  /**
   * Wykonuje operację na przekazanym argumencie. Nic nie zwraca.
   */
  void accept(T t);
​
  /**
   * Pozwala łączyć operacje Consumera
   */
  default java.util.function.Consumer<T> andThen(java.util.function.Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
  }
}

Na początek sprawdźmy co robi metoda accept. Wykorzystamy do tego pętlę forEach, która jak się okazuje może przyjąć jako argument obiekt typu Consumer. Tworzymy listę książek, potem wyrażenie lambda w oparciu o Consumera, którego zadaniem jest przyjąć obiekt i wypisać tytuł. Na końcu wywołujemy forEach na naszej liście i przekazujemy tam naszą lambdę.

List<Book> list = Arrays.asList(
      new Book(19.99, "Czysty kod", "twarda"),
      new Book(29.99, "Pani jeziora", "miękka"),
      new Book(39.99, "Hobbit","twarda"));
​
    Consumer<Book> printTitle = (book) -> System.out.println(book.title);
    list.forEach(printTitle);

Wynik:

Czysty kod
Pani jeziora
Hobbit

Żeby zaprezentować andThen tworzymy jeszcze jedno wyrażenie lambda, które wydrukuje rodzaj okładki. Przekazujemy obydwa wyrażenia do pętli łącząc je za pomocą andThen. Najpierw wywoływane jest printTitle potem printCover na każdym obiekcie.

  Consumer<Book> printCover = (book) -> System.out.println(book.cover);
  list.forEach(printTitle.andThen(printCover));

Wynik:

Czysty kod
twarda
Pani jeziora
miękka
Hobbit
twarda

Function

Function przyjmuje argument i zwraca wynik. Zawiera klika metod deafult, które sobie także omówimy. Zobaczmy jak wygląda ten interfejs funkcjonalny:

@FunctionalInterface
public interface Function<T, R> {

  /**
   * Wykonuje operacje i zwraca wynik
   */
  R apply(T t);

  /**
   * Metoda najpierw przetwarza argument @before, po czym wynik przetwarzany jest przez lambdę
   * na której wywołana została metoda compose.
   */
  default <V> java.util.function.Function<V, R> compose(java.util.function.Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
  }

  /**
   * Metoda wykonuje najpierw apply na wyrażeniu lambdy, na której zostła wyowłana metoda andThen
   * i potem wykonuje lambdę przesłaną jako argument.
   */
  default <V> java.util.function.Function<T, V> andThen(java.util.function.Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
  }

  /**
   * Zwraca funkcję, która zwraca argument wejściowy
   */
  static <T> java.util.function.Function<T, T> identity() {
    return t -> t;
  }
}

Na początek metoda abstrakcyjna apply. Definiujemy wyrażenie lambda, którego zadaniem jest przetworzenie ceny do stringa. Funkcja przetwarza naszego doubla i zwraca już stringa, którego sobie drukujemy:

Book book1 = new Book(19.99, "Czysty kod", "twarda");
Function<Book, String> returnPrice = (book) -> Double.toString(book.price);
​
 System.out.println(returnPrice.apply(book1));

Wynik:

19.99

Następną metodą, jest metoda default compose. Przetwarza ona nasze wywołania nieco od tyłu. Najpierw wykorzystujemy lambdę zdefiniowaną wyżej. Zwraca nam cenę w postaci stringa, potem przesyłamy wynik do lambdy addString:

Function<String, String> addString = (s) -> "Cena: " +s;
 System.out.println(addString.compose(returnPrice).apply(book1));

Wynik:

Cena: 19.99

Metoda addThen działa na tych samych lambdach, które zdefiniowaliśmy wyżej. Przetwarza je w kolejności wywołania. Najpierw zwracana jest cena przez returnPrice, a potem doklejany string przez addString:

System.out.println(returnPrice.andThen(addString).apply(book1));

Wynik:

Cena: 19.99

Ostatnia metoda to identity, która jest metodą statyczną. Jak wnioskujemy z opisu, zwraca ona przekazany do niej wynik. Możemy ją zdefiniować i wywołać tak:

Function<Book, Book> returnBook = Function.identity();
System.out.println(returnBook.apply(book1));

To samo otrzymamy jeśli zrobimy to tak:

Function<Book, Book> returnBook2 = book -> book;
System.out.println(returnBook2.apply(book1));

Wynik:

Book{title='Czysty kod', price=19.99, cover='twarda'}

UnaryOperator

Kolejny interfejs funkcjonalny UnaryOperator<T> rozszerza Function<T,T> tym samym przejmując jegoy metod. Wygląda tak:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
​
  /**
   *Zwraca argument wejściowy
   */
   static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

Jak widzimy posiada własną implementację metody identity(). Ważną kwestią tego interfejsu jest to, że pracuje on zawsze na jednym typie obiektu. W interfejsie Function<T,T> mogliśmy zdefiniować lambdę operującą na obiekcie Book, która zwracała stringa. Tutaj tego nie możemy zrobić.

Lambda stworzona na UnaryOperator<T> zwraca ten sam typ obiektu. W tym przypadku tworzymy obiekt someBook. Tworzona lambda ma za zadanie zwrócić kopię tego obiektu.

Book someBook = new Book(19.99, "Czysty kod", "twarda");
UnaryOperator<Book> calculateDiscount = book -> book.clone();
​
Book bookCopy = calculateDiscount.apply(someBook);
System.out.println("Czy to te same obiekty? ");
System.out.println(someBook == bookCopy);

Wynik:

Czy to te same obiekty?
false

Pamiętajcie, że wszystkie przykłady umieszczone są na git