Skip to content

4. Лямбда-функції в Java 8

4.1. Приклад-01 — Функціональні інтерфейси та лямбда-функції

  

Розглянемо такий код:


package dvp.java8.lambdas;

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

    // анонімні класи
    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);
      }
    };

    // лямбда-вирази
    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);
    };

    // додаток
    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);
}
  • рядки 41–44: визначають функціональний інтерфейс I1. Функціональний інтерфейс — це інтерфейс, що має лише один метод. Він не пов’язаний із наявністю анотації [@FunctionalInterface] у рядку 41, яка є необов’язковою;
  • рядки 46–49: другий функціональний інтерфейс I2;
  • рядки 6–19: інтерфейси I1 та I2 реалізовано за допомогою анонімних класів — це було найпоширенішим рішенням до впровадження лямбда-функцій;
  • рядки 21–29: інтерфейси I1 та I2 реалізовано за допомогою лямбда-функцій;
  • рядки 7–12: реалізація інтерфейсу I1 за допомогою анонімного класу. Синтаксис реалізації інтерфейсу I за допомогою анонімного класу такий:
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

де m1, m2, ... — методи, визначені інтерфейсом I.

  • рядки 14–19: реалізація інтерфейсу I2 за допомогою анонімного класу;
  • рядок 22: реалізація інтерфейсу I1 за допомогою лямбда-функції. Тут використовується той факт, що функціональний інтерфейс має лише один метод. Лямбда-функція реалізує цей єдиний метод M. Її синтаксис такий:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

Типи T1, T2, Tn можна опустити, якщо компілятор може вивести їх із контексту (виведення типу).

  • рядок 22: реалізація методу [I1.doSomething] із сигнатурою:

void doSomething();

[doSomething] — це метод, який не має параметрів і не повертає результату. Його лямбда-реалізацію можна записати так, як у рядку 22, або ж так, як у рядках 24–26, тобто можна поставити фігурні дужки навколо коду лямбда-функції. Якщо цей код містить лише одну інструкцію, як у цьому випадку, ці фігурні дужки можна опустити;

  • рядок 23: реалізація методу [I1.getSomething] із сигнатурою:

String getSomething(double value);

[getSomething] приймає параметр типу [double] і повертає результат типу [String]. Його лямбда-реалізація може бути такою, як у рядку 23, або такою, як у рядках 27–29. У реалізації з рядка 23:

  • тип параметра [value] опущено. У такому разі буде використано тип [double], знайдений у сигнатурі [getSomething];
  • код лямбда-виразу не взято в дужки. У результаті лямбда-вираз дорівнює значенню єдиного виразу в цьому коді, а саме: String.format("ib2.lambda(%s)", value);

У реалізації рядків 27–29:

  • явно оголошується тип параметра [value];
  • Для повернення результату лямбда-виразу використовується [return]. У цьому випадку потрібно вказати фігурні дужки;
  • рядки 32–37: викликаються різні анонімні функції та лямбда-вирази;

Отримано такий результат:

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. Приклад-02 — функціональний інтерфейс Predicate<T>

  

Здебільшого ми маємо справу з функціональними інтерфейсами бібліотек, а не з тими, які ми визначаємо самостійно. Тут нас цікавить функціональний інтерфейс [Predicate], визначений у пакеті [java.util.function], який об’єднує більшість функціональних інтерфейсів Java 8. Він визначений таким чином:

Image

Ми вже зазначали, що функціональний інтерфейс має лише один метод. Однак тут їх чотири. Ще одним нововведенням Java 8 стало введення поняття методу за замовчуванням в інтерфейсі, що позначається ключовим словом [default]. Тут ми маємо три таких методи. Особливістю цих методів є наявність реалізації за замовчуванням. Отже, клас, що реалізує інтерфейс із методами за замовчуванням, не зобов’язаний реалізовувати ці методи. Таким чином, клас, який хоче реалізувати інтерфейс [Predicate], повинен обов’язково реалізувати лише один метод: метод [test]. Отже, інтерфейс [Predicate] є саме функціональним інтерфейсом. Таким чином, функціональний інтерфейс — це інтерфейс, реалізація якого містить лише один обов’язковий метод. Якщо він має більше одного методу, інші методи повинні мати ключове слово [default].

Метод [Predicate<T>.test] приймає параметр типу T і повертає логічне значення. Цей інтерфейс зазвичай використовується для фільтрування колекцій. Щоб проілюструвати його використання, ми скористаємося такими даними:


package dvp.data;

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

public class Personne {

  public enum Sexe {HOMME,FEMME};

  // дані
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // конструктори
  public Personne() {

  }

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

  // геттери та сеттери
...

  // toString
  @Override
  public String toString(){
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • рядки 32–38: метод [toString] повертає рядок jSON, що відповідає особі;

Клас [Personnes] визначає список із 3 осіб:


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();
    }
  }
}
  • рядки 10–11: список із 3 осіб;
  • рядки 13–15: статичний метод для отримання цього списку;
  • рядки 17–23: статичний метод, що дозволяє отримати рядок jSON зі списку осіб, переданого як параметр;

Ці дані будуть оброблятися наступним кодом:


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) {
    // предикат, реалізований анонімним класом
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// предикат, реалізований за допомогою лямбда-виразу
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // список осіб
    List<Personne> personnes = Personnes.get();
    // перегляди
    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] фільтрує список [personnes]
    List<Personne> personnesFiltrées = new ArrayList<>();
    for (Personne p : personnes) {
      if (filter.test(p)) {
        personnesFiltrées.add(p);
      }
    }
    return personnesFiltrées;  }
}
  • рядки 13–18: реалізація інтерфейсу Predicate<Personne> за допомогою анонімного класу. Це фільтр за вагою особи;
  • рядок 20: реалізація інтерфейсу Predicate<Personne> за допомогою лямбда-функції. Це фільтр за віком особи. Згідно з тим, що було сказано, її також можна було б написати таким чином:

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

але варіант у рядку 20 є більш лаконічним. Тип параметра p виводиться з контексту. Тут створюється тип [Predicate<Personne>]. Тоді сигнатура реалізованого методу має вигляд [boolean test(Personne param)]. Отже, неявним типом p у рядку 20 є тип [Personne];

  • рядок 22: отримуємо заздалегідь визначений список осіб;
  • рядок 24: їх фільтрують за віком;
  • рядок 25: фільтруємо їх за вагою. В обох випадках виводимо рядок jSON із відфільтрованого списку;
  • рядки 28–37: статичний метод, який
    • приймає як параметри: список осіб, що підлягають фільтруванню, та фільтр. Останній є екземпляром інтерфейсу [Predicate<Personne>]. У неформальному вживанні терміна тут та в інших частинах документа під екземпляром інтерфейсу I мається на увазі екземпляр класу C, що реалізує I;
    • повертає у результаті відфільтрований список;
  • рядок 32: використовується метод [test] інтерфейсу [Predicate]. Залежно від фільтра, переданого методу, метод [test] буде:

return personne.getPoids()<50;

або


return p.getAge() < 28

Виконання класу [Exemple02] дає такий результат:

[{"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. Приклад-03 — функціональний інтерфейс Function<T,R>

  

Функціональний інтерфейс Function<T,R> визначається наступним чином:

Image

Єдиний метод інтерфейсу має сигнатуру R apply(T t). Зазвичай його використовують для створення нового типу Collection<R> на основі типу Collection<T>. Для ілюстрації цього інтерфейсу ми використаємо такий код:


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 {
    // реалізація з використанням анонімного класу
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // реалізація з використанням лямбда-виразу
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // список осіб
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // відображення
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }

  // перетворення List<Personne> --> 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;
  }
}
  • рядки 15–20: реалізуємо інтерфейс [Function<Personne, String>] за допомогою анонімного класу, який здійснюватиме перетворення Personne --> String;
  • рядок 22: реалізуємо інтерфейс [Function<Personne, Integer>] за допомогою лямбда-функції, яка здійснюватиме перетворення Personne --> Integer;
  • рядки 33–39: статичний метод, який
    • приймає два параметри: перший типу List<Personne> — це список осіб, які підлягають перетворенню. Другий типу Function<Personne, T> — це функція, яка на основі кожної особи зі списку створює об’єкт типу T;
    • повертає у результаті тип List<T>, де кожен елемент є результатом перетворення «Особа» → T;
  • рядки 35–37: застосовується перетворення «Особа» → T. Якщо другим параметром методу є об’єкт типу [mapToName], то буде виконано перетворення «Особа» → String. Якщо це об’єкт [mapToAge], буде виконано перетворення «Особа» → «Ціле»;

Отриманий результат такий:

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

4.4. Приклад-04 — функціональний інтерфейс Consumer<T>

  

Функціональний інтерфейс Consumer<T> визначається наступним чином:

Image

Єдиний метод інтерфейсу має сигнатуру: void accept(T t). Цей метод обробляє (споживає) свій параметр і не повертає жодного результату. Для ілюстрації ми використаємо такий код:


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) {
        // список осіб
        List<Personne> personnes = Personnes.get();
        // анонімна реалізація
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // реалізація лямбда
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // виведення
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • рядки 14–19: інтерфейс [Consumer<Personne>] реалізовано анонімним класом, метод [accept] якого виводить ім’я та вік особи;
  • рядок 21: інтерфейс [Consumer<Personne>] реалізовано за допомогою лямбда-функції, неявний метод якої [accept] виводить ім’я та вагу людини;
  • рядки 23–25: список осіб використовується реалізацією [consumerAge];
  • рядки 27–29: список осіб використовується реалізацією [consumerPoids];

Результати такі:

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. Приклад-05 — функціональний інтерфейс BiConsumer<T,U>

  

Функціональний інтерфейс BiConsumer<T,U> визначено таким чином:

Image

Єдиний метод інтерфейсу має сигнатуру: void accept(T t, U u). Він приймає тип T із додатковою інформацією U u. Проілюструємо його використання за допомогою такого коду:


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) {
    // список осіб
    List<Personne> personnes = Personnes.get();
    // анонімна реалізація
    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());
      }
    };
    // реалізація лямбда
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // перегляди
    for (Personne p : personnes) {
      biconsumerAge.accept(p, 100);
    }
    System.out.println("--------");
    for (Personne p : personnes) {
      biconsumerPoids.accept(p, 200);
    }
  }
}
  • рядки 14–20: реалізація інтерфейсу BiConsumer<T,U> за допомогою анонімного класу. Метод [apply] використовує свій другий параметр для оновлення віку особи, переданої як перший параметр. Потім він виводить результат;
  • рядки 22–25: реалізація інтерфейсу BiConsumer<T,U> за допомогою лямбда-функції. Неявний метод [apply] використовує свій другий параметр для оновлення ваги особи, переданої як перший параметр. Потім він виводить результат;
  • рядки 27–29: список осіб обробляється за допомогою реалізації [biconsumerAge];
  • рядки 31–33: список осіб обробляється за допомогою реалізації [biconsumerPoids];

Отримано такі результати:

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. Приклад-06 — функціональний інтерфейс BiFunction<T,U,R>

  

Функціональний інтерфейс BiFunction<T,U,R> визначається наступним чином:

Image

Єдиний метод інтерфейсу має сигнатуру: R apply(T t, U u). Цей метод схожий на метод [BiConsumer.apply], але на відміну від останнього, який не повертає результату, метод [BiFunction.apply] повертає результат. Проілюструємо його використання за допомогою такого коду:


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) {
        // список осіб
        List<Personne> personnes = Personnes.get();
        // анонімна реалізація
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // реалізація лямбда
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // перегляди
        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));
        }
    }
}
  • рядки 14–19: інтерфейс BiFunction<Personne, Integer, Integer> реалізовано за допомогою анонімного класу. Метод [apply] цього класу повертає вік особи, переданої як перший параметр, збільшений на значення другого параметра;
  • рядки 21–23: інтерфейс BiFunction<Personne, Integer, Double> реалізовано за допомогою лямбда-функції. Метод [apply] повертає вагу особи, переданої в першому параметрі, збільшену на значення другого параметра;
  • рядки 25–27: до осіб застосовується реалізація [biFunctionAge];
  • рядки 29–31: до осіб застосовується реалізація [biFunctionPoids];

Отримано такі результати:

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

Окрім лямбда-функцій, у Java 8 було введено тип Stream<T>, який моделює потік елементів типу T. Ці елементи можуть піддаватися послідовним перетворенням, реалізованим за допомогою лямбда-функцій. Якщо це можливо і є кілька процесорів, ці перетворення іноді можуть виконуватися паралельно.

4.7. Приклад-07 — функціональний інтерфейс Supplier<T>

  

Функціональний інтерфейс Supplier<T> визначається наступним чином:

Image

Єдиний метод інтерфейсу має сигнатуру: T get(), роль якого полягає в наданні об’єкта типу T.

Проілюструємо цей функціональний інтерфейс за допомогою такого коду:


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) {
        // анонімна реалізація
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // список осіб
            List<Personne> personnes = Personnes.get();

            // реалізація інтерфейсу
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // обробка
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }

    // перегляд особи
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • рядки 13–28: реалізація типу Supplier<Personne>;
  • рядки 31–33: статичний метод [affiche] очікує параметр типу Supplier<Personne>;
  • рядки 25–27: виконання екземпляра Supplier<Personne>;

Отримуємо такі результати:

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"}