Skip to content

4. Funkcje lambda w Javie 8

4.1. Przykład 01 – Interfejsy funkcjonalne i lambda

  

Rozważmy następujący kod:


package dvp.java8.lambdas;

public class Exemple01 {
  public static void main(String[] args) {

    // klasy anonimowe
    I1 ia1 = new I1() {
      @Override
      public void doSomething() {
        System.out.println("ia1.doSomething");
      }
    };

    I2 ia2 = new I2() {
      @Override
      public String getSomething(double value) {
        return String.format("ia2.getSomething(%s)", value);
      }
    };

    // lambdy
    I1 ib1 = () -> System.out.println("ib1.lambda");
    I2 ib2 = (value) -> String.format("ib2.lambda(%s)", value);
    I1 ib3 = () -> {
      System.out.println("ib3.lambda");
    };
    I2 ib4 = (double value) -> {
      return String.format("ib4.lambda(%s)", value);
    };

    // aplikacja
    ia1.doSomething();
    System.out.println(ia2.getSomething(4.3));
    ib1.doSomething();
    System.out.println(ib2.getSomething(5.8));
    ib3.doSomething();
    System.out.println(ib4.getSomething(10.1));
  }
}

@FunctionalInterface
interface I1 {
  void doSomething();
}

@FunctionalInterface
interface I2 {
  String getSomething(double value);
}
  • wiersze 41–44: definiują interfejs funkcjonalny I1. Interfejs funkcjonalny to interfejs posiadający tylko jedną metodę. Nie jest on powiązany z obecnością adnotacji [@FunctionalInterface] w wierszu 41, która jest opcjonalna;
  • wiersze 46–49: drugi interfejs funkcjonalny I2;
  • wiersze 6–19: interfejsy I1 i I2 są zaimplementowane za pomocą klas anonimowych, co było najczęstszym rozwiązaniem przed wprowadzeniem funkcji lambda;
  • wiersze 21–29: interfejsy I1 i I2 są zaimplementowane za pomocą funkcji lambda;
  • wiersze 7–12: implementacja interfejsu I1 za pomocą klasy anonimowej. Składnia implementacji interfejsu I za pomocą klasy anonimowej jest następująca:
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

gdzie m1, m2, ... to metody zdefiniowane przez interfejs I.

  • wiersze 14–19: implementacja interfejsu I2 za pomocą klasy anonimowej;
  • wiersz 22: implementacja interfejsu I1 za pomocą funkcji lambda. Wykorzystujemy tutaj fakt, że interfejs funkcjonalny ma tylko jedną metodę. Funkcja lambda implementuje zatem tę jedyną metodę M. Jej składnia jest następująca:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

Typy T1, T2, Tn można pominąć, jeśli kompilator może je wywnioskować z kontekstu (wnioskowanie typu).

  • wiersz 22: implementacja metody [I1.doSomething] o sygnaturze:

void doSomething();

[doSomething] to metoda, która nie ma żadnych parametrów i nie zwraca wyniku. Jej implementację lambda można zapisać tak jak w wierszu 22 lub tak jak w wierszach 24–26, tzn. można umieścić kody funkcji lambda w nawiasach klamrowych. Jeśli kod ten zawiera tylko jedną instrukcję, jak w tym przypadku, nawiasy klamrowe można pominąć;

  • wiersz 23: implementacja metody [I1.getSomething] o sygnaturze:

String getSomething(double value);

[getSomething] przyjmuje parametr typu [double] i zwraca wynik typu [String]. Jej implementacja lambda może być taka jak w wierszu 23 lub taka jak w wierszach 27–29. W implementacji z linii 23:

  • typ parametru [value] jest pominięty. W takim przypadku zostanie użyty typ [double] znaleziony w sygnaturze funkcji [getSomething];
  • kod lambda nie jest ujęty w nawiasy. Wynikiem lambda jest wówczas wartość jedynego wyrażenia w tym kodzie, w tym przypadku: String.format("ib2.lambda(%s)", value);

W implementacji w wierszach 27–29:

  • wyraźnie deklaruje się typ parametru [value];
  • Do zwrócenia wyniku funkcji lambda używa się [return]. W tym przypadku należy użyć nawiasów klamrowych;
  • wiersze 32–37: wywołuje się różne funkcje anonimowe i lambda;

Otrzymany wynik jest następujący:

1
2
3
4
5
6
ia1.doSomething
ia2.getSomething(4.3)
ib1.lambda
ib2.lambda(5.8)
ib3.lambda
ib4.lambda(10.1)

4.2. Przykład 02 – interfejs funkcjonalny Predicate<T>

  

W większości przypadków mamy do czynienia z interfejsami funkcjonalnymi bibliotek, a nie z interfejsami funkcjonalnymi, które sami definiujemy. W tym przypadku interesuje nas interfejs funkcjonalny [Predicate] zdefiniowany w pakiecie [java.util.function], który zawiera większość interfejsów funkcjonalnych z Javy 8. Jest on zdefiniowany w następujący sposób:

Image

Wspomnieliśmy, że interfejs funkcjonalny ma tylko jedną metodę. Tutaj jednak są cztery. Kolejną innowacją wprowadzoną przez Javę 8 było pojęcie metody domyślnej w interfejsie, oznaczonej słowem kluczowym [default]. Mamy tu trzy takie metody. Cechą charakterystyczną tych metod jest to, że mają one domyślną implementację. Nie ma zatem obowiązku, aby klasa implementująca interfejs z metodami domyślnymi musiała je implementować. W ten sposób klasa, która chce implementować interfejs [Predicate], musi obowiązkowo zaimplementować tylko jedną metodę: metodę [test]. Interfejs [Predicate] jest zatem interfejsem funkcjonalnym. Można zatem stwierdzić, że interfejs funkcjonalny to taki, którego implementacja zawiera tylko jedną obowiązkową metodę. Jeśli posiada więcej niż jedną metodę, pozostałe muszą mieć słowo kluczowe [default].

Metoda [Predicate<T>.test] przyjmuje parametr typu T i zwraca wartość logiczną. Interfejs ten jest zazwyczaj wykorzystywany do filtrowania kolekcji. Aby zilustrować jego zastosowanie, posłużymy się następującymi danymi:


package dvp.data;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Personne {

  public enum Sexe {HOMME,FEMME};

  // dane
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // konstruktory
  public Personne() {

  }

  public Personne(String nom, Sexe sexe, int age, double poids) {
    this.nom = nom;
    this.sexe=sexe;
    this.age = age;
    this.poids = poids;
  }

  // metody pobierające i ustawiające
...

  // toString
  @Override
  public String toString(){
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • wiersze 32–38: metoda [toString] zwraca ciąg znaków jSON danej osoby;

Klasa [Personnes] definiuje listę trzech osób:


package dvp.data;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

public class Personnes {
  private static List<Personne> personnes = Arrays.asList(new Personne("jean", Personne.Sexe.HOMME, 20, 70),
    new Personne("marie", Personne.Sexe.FEMME, 10, 30), new Personne("camille", Personne.Sexe.FEMME, 30, 55));

  public static List<Personne> get() {
    return personnes;
  }

  public static String toString(List<Personne> liste) {
    try {
      return new ObjectMapper().writeValueAsString(liste);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • wiersze 10–11: lista trzech osób;
  • wiersze 13–15: metoda statyczna służąca do uzyskania tej listy;
  • wiersze 17–23: metoda statyczna umożliwiająca uzyskanie ciągu znaków jSON z listy osób przekazanej jako parametr;

Dane te zostaną wykorzystane przez następujący kod:


package dvp.java8.lambdas;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple02 {
  public static void main(String[] args) {
    // predykat zaimplementowany przez klasę anonimową
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// predykat zaimplementowany przez lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // lista osób
    List<Personne> personnes = Personnes.get();
    // wyświetlenia
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterAge)));
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterPoids)));
  }

  private static List<Personne> filterPersonnes(List<Personne> personnes, Predicate<Personne> filter) {
      // [filter] filtruje listę [personnes]
    List<Personne> personnesFiltrées = new ArrayList<>();
    for (Personne p : personnes) {
      if (filter.test(p)) {
        personnesFiltrées.add(p);
      }
    }
    return personnesFiltrées;  }
}
  • wiersze 13–18: implementacja interfejsu Predicate<Osoba> za pomocą klasy anonimowej. Jest to filtr oparty na wadze osoby;
  • wiersz 20: implementacja interfejsu Predicate<Osoba> za pomocą funkcji lambda. Jest to filtr oparty na wieku osoby. Zgodnie z powyższym wyjaśnieniem można by go również zapisać w następujący sposób:

        Predicate<Personne> filterAge = (Personne p) -> {
            return (p.getAge() < 28);
        };

ale wersja z linii 20 jest bardziej zwięzła. Typ parametru p jest wywnioskowany z kontekstu. W tym miejscu tworzony jest typ [Predicate<Personne>]. Zaimplementowana metoda ma zatem sygnaturę [boolean test(Personne param)]. Zatem typ domyślny dla p w wierszu 20 to typ [Personne];

  • wiersz 22: pobieramy wstępnie zdefiniowaną listę osób;
  • wiersz 24: filtruje się je według wieku;
  • wiersz 25: filtruje się je według wagi. W obu przypadkach wyświetla się ciąg znaków jSON z tak przefiltrowanej listy;
  • wiersze 28–37: metoda statyczna, która
    • przyjmuje jako parametry: listę osób do przefiltrowania oraz filtr. Ten ostatni jest instancją interfejsu [Predicate<Personne>]. W sensie przenośnym, zarówno tutaj, jak i w innych miejscach dokumentu, przez instancję interfejsu I rozumiemy instancję klasy C implementującej I;
    • zwraca jako wynik tak przefiltrowaną listę;
  • wiersz 32: wykorzystuje się metodę [test] z interfejsu [Predicate]. W zależności od filtru przekazanego do tej metody, metoda [test] będzie:

return personne.getPoids()<50;

lub


return p.getAge() < 28

Wywołanie klasy [Exemple02] daje następujący wynik:

[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

4.3. Przykład 03 – interfejs funkcjonalny Function<T,R>

  

Interfejs funkcjonalny Function<T,R> jest zdefiniowany w następujący sposób:

Image

Jedyna metoda tego interfejsu ma sygnaturę R apply(T t). Zazwyczaj służy ona do tworzenia nowego typu Collection<R> na podstawie typu Collection<T>. Aby zilustrować działanie tego interfejsu, wykorzystamy następujący kod:


package dvp.java8.lambdas;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.data.Personne;
import dvp.data.Personnes;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class Exemple03 {
  public static void main(String[] args) throws JsonProcessingException {
    // implementacja z wykorzystaniem klasy anonimowej
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implementacja z użyciem lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // lista osób
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // wyświetlenia
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }

  // konwersja List<Osoba> --> List<T>
  private static <T> List<T> mapPersonnes(List<Personne> personnes, Function<Personne, T> mapper) {
    List<T> maps = new ArrayList<>();
    for (Personne p : personnes) {
      maps.add(mapper.apply(p));
    }
    return maps;
  }
}
  • wiersze 15–20: implementujemy interfejs [Function<Personne, String>] za pomocą klasy anonimowej, która dokona transformacji Personne --> String;
  • wiersz 22: implementujemy interfejs [Function<Personne, Integer>] za pomocą funkcji lambda, która przeprowadzi transformację Personne --> Integer;
  • wiersze 33–39: metoda statyczna, która
    • przyjmuje dwa parametry: pierwszy typu List<Personne> to lista osób do przekształcenia. Drugi typu Function<Osoba, T> to funkcja, która na podstawie każdej osoby z listy tworzy obiekt typu T;
    • zwraca wynik typu List<T>, gdzie każdy element pochodzi z transformacji Osoba -> T;
  • wiersze 35–37: stosuje się transformację Personne -> T. Jeśli drugim parametrem metody jest obiekt typu [mapToName], zostanie przeprowadzona transformacja Personne -> String. Jeśli jest to obiekt [mapToAge], zostanie przeprowadzona transformacja Osoba -> Integer;

Otrzymany wynik jest następujący:

["jean","marie","camille"]
[20,10,30]

4.4. Przykład-04 – interfejs funkcjonalny Consumer<T>

  

Interfejs funkcjonalny Consumer<T> jest zdefiniowany w następujący sposób:

Image

Jedyna metoda tego interfejsu ma sygnaturę: void accept(T t). Metoda ta przetwarza (konsumuje) swój parametr i nie zwraca żadnego wyniku. Aby to zilustrować, użyjemy następującego kodu:


package dvp.java8.lambdas;

import java.util.List;
import java.util.function.Consumer;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple04 {
    public static void main(String[] args) {
        // lista osób
        List<Personne> personnes = Personnes.get();
        // anonimowa implementacja
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // implementacja lambda
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // wyświetlenia
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • wiersze 14–19: interfejs [Consumer<Personne>] jest zaimplementowany przez klasę anonimową, której metoda [accept] wyświetla imię i wiek osoby;
  • wiersz 21: interfejs [Consumer<Personne>] jest zaimplementowany przez funkcję lambda, której domyślna metoda [accept] wyświetla imię i wagę osoby;
  • wiersze 23–25: lista osób jest wykorzystywana przez implementację [consumerAge];
  • wiersze 27–29: lista osób jest przetwarzana przez implementację [consumerPoids];

Wyniki są następujące:

1
2
3
4
5
6
7
 age de jean = 20
 age de marie = 10
 age de camille = 30
--------
 poids de jean = 70.0
 poids de marie = 30.0
poids de camille = 55.0

4.5. Przykład-05 – interfejs funkcjonalny BiConsumer<T,U>

  

Interfejs funkcjonalny BiConsumer<T,U> jest zdefiniowany w następujący sposób:

Image

Jedyna metoda tego interfejsu ma sygnaturę: void accept(T t, U u). Przyjmuje ona typ T wraz z dodatkową informacją U u. Jej zastosowanie zilustrujemy za pomocą poniższego kodu:


package dvp.java8.lambdas;

import dvp.data.Personne;
import dvp.data.Personnes;

import java.util.List;
import java.util.function.BiConsumer;

public class Exemple05 {
  public static void main(String[] args) {
    // lista osób
    List<Personne> personnes = Personnes.get();
    // implementacja anonimowa
    BiConsumer<Personne, Integer> biconsumerAge = new BiConsumer<Personne, Integer>() {
      @Override
      public void accept(Personne personne, Integer integer) {
        personne.setAge(personne.getAge() + integer);
        System.out.printf("age de %s = %s%n", personne.getNom(), personne.getAge());
      }
    };
    // implementacja lambda
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // wyświetlenia
    for (Personne p : personnes) {
      biconsumerAge.accept(p, 100);
    }
    System.out.println("--------");
    for (Personne p : personnes) {
      biconsumerPoids.accept(p, 200);
    }
  }
}
  • wiersze 14–20: implementacja interfejsu BiConsumer<T,U> za pomocą klasy anonimowej. Metoda [apply] wykorzystuje swój drugi parametr do aktualizacji wieku osoby przekazanej jako pierwszy parametr. Następnie wyświetla wynik;
  • wiersze 22–25: implementacja interfejsu BiConsumer<T,U> za pomocą funkcji lambda. Metoda domyślna [apply] wykorzystuje swój drugi parametr do aktualizacji wagi osoby przekazanej jako pierwszy parametr. Następnie wyświetla wynik;
  • wiersze 27–29: lista osób jest przetwarzana przy użyciu implementacji [biconsumerAge];
  • wiersze 31–33: lista osób jest przetwarzana za pomocą implementacji [biconsumerPoids];

Uzyskane wyniki są następujące:

1
2
3
4
5
6
7
age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

4.6. Przykład-06 – interfejs funkcjonalny BiFunction<T,U,R>

  

Interfejs funkcjonalny BiFunction<T,U,R> jest zdefiniowany w następujący sposób:

Image

Jedyna metoda tego interfejsu ma sygnaturę: R apply(T t, U u). Metoda ta jest podobna do metody [BiConsumer.apply], jednak podczas gdy ta ostatnia nie zwraca wyniku, metoda [BiFunction.apply] zwraca wynik. Jej zastosowanie zilustrujemy za pomocą następującego kodu:


package dvp.java8.lambdas;

import java.util.List;
import java.util.function.BiFunction;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple06 {
    public static void main(String[] args) {
        // lista osób
        List<Personne> personnes = Personnes.get();
        // implementacja anonimowa
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // implementacja lambda
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // wyświetlenia
        for (Personne p : personnes) {
            System.out.printf("age de %s = %s%n", p.getNom(), biFunctionAge.apply(p, 100));
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            System.out.printf("poids de %s = %s%n", p.getNom(), biFunctionPoids.apply(p, 200));
        }
    }
}
  • wiersze 14–19: interfejs BiFunction<Personne, Integer, Integer> jest zaimplementowany za pomocą klasy anonimowej. Metoda [apply] tej klasy zwraca wiek osoby przekazanej jako pierwszy parametr, powiększony o wartość drugiego parametru;
  • wiersze 21–23: interfejs BiFunction<Osoba, Integer, Double> jest zaimplementowany za pomocą funkcji lambda. Metoda [apply] zwraca masę osoby przekazanej jako pierwszy parametr powiększoną o wartość drugiego parametru;
  • wiersze 25–27: stosuje się implementację [biFunctionAge] do osób;
  • wiersze 29–31: stosuje się implementację [biFunctionPoids] do osób;

Uzyskane wyniki są następujące:

age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

Oprócz funkcji lambda w Javie 8 wprowadzono typ Stream<T>, który modeluje strumień elementów typu T. Elementy te mogą być poddawane kolejnym transformacjom zaimplementowanym przez funkcje lambda. Jeśli to możliwe i dostępnych jest wiele procesorów, transformacje te mogą czasami przebiegać równolegle.

4.7. Przykład-07 – interfejs funkcjonalny Supplier<T>

  

Interfejs funkcjonalny Supplier<T> jest zdefiniowany w następujący sposób:

Image

Jedyna metoda tego interfejsu ma sygnaturę: T get(), której zadaniem jest zwracanie obiektu typu T.

Zilustrujemy ten interfejs funkcjonalny za pomocą następującego kodu:


package dvp.java8.lambdas;

import java.util.List;
import java.util.Random;
import java.util.function.Supplier;

import dvp.data.Personne;
import dvp.data.Personnes;

public class Exemple07 {
    public static void main(String[] args) {
        // implementacja anonimowa
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // lista osób
            List<Personne> personnes = Personnes.get();

            // implementacja interfejsu
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // analiza
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }

    // wyświetlanie osoby
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • wiersze 13–28: implementacja typu Supplier<Personne>;
  • wiersze 31–33: metoda statyczna [affiche] oczekuje parametru typu Supplier<Personne>;
  • wiersze 25–27: uruchomienie instancji Supplier<Personne>;

Otrzymujemy następujące wyniki:

1
2
3
4
5
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}