Java. Лямбда-вирази. Основні поняття. Функціональний інтерфейс. Узагальнені функціональні інтерфейси та лямбда-вирази. Приклади




Java. Лямбда-вирази. Основні поняття. Функціональний інтерфейс. Узагальнені функціональні інтерфейси та лямбда-вирази. Приклади


Зміст


Пошук на інших ресурсах:

1. Поняття про лямбда-вирази. Переваги застосування лямбда-виразів

Лямбда-вирази з’явились у версії JDK 8 з метою удосконалення мови Java. Лямбда-вирази ще називають замиканнями.

До переваг застосування лямбда-виразів у мові Java можна віднести наступні:

  • використання нових елементів, які підвищують виразність мови Java. Спрощується реалізація деяких загальновживаних конструкцій;
  • розширення можливостей бібліотеки програмного інтерфейсу (API). Сюди можна віднести спрощення паралельної обробки, покращення роботи з потоками вводу/виводу в програмному інтерфейсі API.

Поява лямбда-виразів сприяла виникненню таких нових засобів мови Java, як методи за замовчуванням та використання посилань на метод без його виконання.

 

2. Лямбда-вираз (замикання). Загальна форма. Лямбда-оператор –>. Приклади

Реалізація будь-якого лямбда-виразу базується на використанні двох мовних конструкцій:

  • безпосередньо лямбда-виразу;
  • функціонального інтерфейсу.

Лямбда-вираз – це є анонімний (безіменний) метод, який не виконується самостійно, а служить реалізацією методу, оголошеному в функціональному інтерфейсі (дивіться пункт 3). Якщо у програмі зустрічається лямбда-вираз, то це призводить до створення деякого анонімного класу, що містить анонімний метод, код якого визначається у лямбда-виразі.

При оголошенні лямбда-виразу використовується лямбда-оператор, який позначається символами –> (стрілка). Лямбда-оператор трактується як “стає” або “переходить”. Лямбда-оператор (–>) розділяє лямбда-вираз на дві частини: ліву та праву. У лівій частині лямбда-виразу вказуються параметри. У правій частині вказуються дії (оператори), які визначають код лямбда-виразу.

Код лямбда-виразу може формуватись одним з двох способів:

  • містити одиночний вираз. У цьому випадку лямбда-вираз називається одиночним;
  • взятий у фігурні дужки { }. Це випадок, коли в правій частині лямбда-оператора потрібно вказати декілька операторів. У цьому випадку лямбда-вираз називається блочним. При поверненні результату у блочних лямбда-виразах обов’язково потрібно вказувати оператор return.

У найпростішому випадку, оголошення блочного лямбда-виразу може бути наступним:

(list_of_parameters) -> {
  // Тіло лямбда-виразу
  // ...
  return result;
}

тут

  • list_of_parameters – список параметрів, які отримує лямбда-вираз. Список параметрів задається через кому так само як в методі. Список параметрів вказується у лівій частині лямбда-оператора. Параметри лямбда-виразу повинні бути сумісні за типом та кількістю з параметрами абстрактного методу, який оголошений в функціональному інтерфейсі.

Якщо в лямбда-виразі міститься єдиний параметр, він може бути без круглих дужок ( ):

i -> {
  // ...
}

Можлива ситуація, коли лямбда-вираз не отримує параметрів. У цьому випадку Загальна форма блочного лямбда-виразу наступна

() -> {
  // Тіло лямбда-виразу
  // ...
}

Якщо в лямбда-виразі використовується один оператор (вираз), то фігурні дужки можна опустити:

(list_of_parameters) -> expression;

тут

  • list_of_parameters – список параметрів;
  • expression – вираз, який буде обчислений при використанні лямбда-виразу.

Приклад 1. Нижче наведено лямбда-вираз без параметрів, який повертає число π:

() -> Math.PI

У вищенаведеному коді результатом лямбда-виразу служить константа PI з класу Math.

Можна було написати інше рішення

() -> 3.1415

Приклад 2. Лямбда-вираз, який отримує два цілочисельні параметри та повертає їх добуток.

(int a, int b) -> a*b

Приклад 3. Лямбда-вираз, який за довжинами трьох сторін a, b, c повертає площу трикутника.

(double a, double b, double c ) -> {
  if (((a+b)<c) || ((a+c)<b) || ((b+c)<a))
    return 0.0;
  else
  {
    double p = (a+b+c)/2; // півпериметер
    double s = Math.sqrt(p*(p-a)*(p-b)*(p-c)); // площа за формулою Герона
    return s;
  }
}

 

3. Функціональний інтерфейс. Визначення. Загальна форма. Приклади

Функціональний інтерфейс – це інтерфейс, який містить тільки один абстрактний метод. Функціональний інтерфейс визначає тільки одну дію (операцію). У функціональному інтерфейсі визначається цільовий тип лямбда-виразу. Функціональний інтерфейс ще називають SAM-типом (Single Abstract Method).

У найбільш загальному випадку оголошення функціонального інтерфейсу виглядає наступним чином:

interface InterfaceName {
  return_type MethodName(list_of_parameters);
}

тут

  • InterfaceName – ім’я функціонального інтерфейсу;
  • MethodName – ім’я методу, який визначає призначення інтерфейсу;
  • return_type – тип, який повертає метод MethodName;
  • list_of_parameters – список параметрів методу MethodName.

Якщо в інтерфейсі оголошується два і більше абстрактних методів, то цей інтерфейс не вважається функціональним інтерфейсом.

Приклад 1. Оголошується функціональний інтерфейс, який визначає абстрактний метод, що не отримує параметрів та повертає ціле число

// Інтерфейс, що визначає метод без параметрів, який повертає ціле число
interface INumber {
  int GetNumber();
}

Код лямбда-виразу, що реалізує даний інтерфейс, може бути, наприклад, таким:

// Лямбда-вираз, що реалізує інтерфейс INumber
INumber lambdaNumber = () -> 255;

Після формування лямбда-виразу можна викликати метод GetNumber() інтерфейсу INumber()

// Використання лямбда виразу у вигляді методу GetNumber()
int number = lambdaNumber.GetNumber(); // number = 255
System.out.println("number = " + number);

Приклад 2. Оголошується функціональний інтерфейс, який визначає метод, що отримує 3 числа типу double та повертає значення типу double

// Інтерфейс, що визначає абстрактний метод, який отримує 3 параметри
// типу double і повертає значення типу double.
interface ICalculation {
  double CalcMethod(double a, double b, double c);
}

Для такої сигнатури методу можна реалізовувати лямбда-вирази, які виконують різноманітні операції над трьома числами, наприклад:

  • обчислюють суму трьох чисел;
  • обчислюють максимальне (мінімальне) значення серед трьох чисел і т.д.

Нижче наведено побудову та використання лямбда-виразу, який обчислює суму трьох цілих чисел

// Побудова лямбда-виразу для інтерфейсу ICalculation
// 1. Формування методу CalcMethod(), який обчислює
//   суму 3-х чисел типу double з допомогою лямбда-виразу
ICalculation ICalc = (a, b, c) -> a+b+c;

// 2. Обчислити суму цілих чисел 1+2+3
int sum = (int)ICalc.CalcMethod(1, 2, 3);
System.out.println("sum = " + sum);

Як видно з вищенаведеного коду, допускається використання цілочисельних типів в методі, що оперує типом double.

 

4. Послідовність кроків при побудові лямбда-виразів

Для того, щоб у програмі використовувати лямбда-вирази потрібно виконати такі кроки:

  1. Оголосити функціональний інтерфейс I, який містить тільки один абстрактний метод M. Сигнатура параметрів методу M та тип повернення результату повинні відповідати розв’язуваній задачі.
  2. Оголосити посилання Ref на функціональний інтерфейс I у методі, де потрібно використовувати лямбда-вираз.
  3. Сформувати код лямбда-виразу згідно з синтаксисом та присвоїти цей код посиланню Ref на функціональний інтерфейс. Кількість параметрів у лямбда-виразі повинна співпадати з кількістю параметрів, що оголошені в методі M функціонального інтерфейсу I.
  4. З допомогою посилання Ref викликати метод M. Виклик виглядає наступним чином:
Ref.M(parameters)

тут parameters – перелік параметрів методу M.

 

5. Розв’язування задач на лямбда-вирази
5.1. Лямбда-вираз, який обробляє три числа

Умова задачі. Створити та викликати лямбда-вираз, який обчислює:

  • суму трьох чисел;
  • максимальне значення з трьох чисел.

Задачу реалізувати з використанням функціонального інтерфейсу.

 

Розв’язок. Текст програми, що розв’язує задачу наступний.

package TrainLambda;

// Оголосити функціональний інтерфейс
interface MyNumber {
  // Метод, який отримує три параметри типу double
  // та повертає результат типу double
  double GetValue(double a, double b, double c);
}

public class TrainLambda01 {

  public static void main(String[] args) {
    // Використання функціонального інтерфейсу MyNumber
    // 1. Оголосити посилання на функціональний інтерфейс
    MyNumber mn;

    // 2. Присвоїти посиланню mn лямбда-вираз, тут створюється
    //     екземпляр класу з реалізацією методу GetValue().
    //     Посилання mn є посиланням на цей екземпляр класу.
    mn=(a,b,c)->a+b+c; // одиночний лямбда-вираз

    // 3. Викликати метод GetValue() і вивести результат
    double res = mn.GetValue(5, 7.5, 3.8);
    System.out.println("res = " + res);

    // --------------------------------
    // 4. Знаходження максимуму з трьох чисел
    //    Сформувати новий блочний лямбда-вираз
    mn=(a,b,c) -> {
      double max=a;
      if (max<b) max=b;
      if (max<c) max=c;
      return max;
    };

    // Виклик методу GetValue() і вивід результату
    double max = mn.GetValue(5.3, 8.3, 4.2);
    System.out.println("max = " + max);
  }
}

Результат виконання програми

res = 16.3

 

5.2. Лямбда-вираз, який використовується в параметрі методу для обробки масиву чисел

Реалізувати та продемонструвати метод, що знаходить суму парних та непарних елементів цілочисельного масиву. Метод повинен отримувати параметром лямбда-вираз, який визначає парність елементу та використовувати його для обчислень. Продемонструвати роботу методу для лямбда-виразів, які:

  • визначають, чи елемент масиву є парний;
  • визначають, чи елемент масиву є непарний. 
package TrainLambda;

// Функціональний інтерфейс, який призначений для перевірки числа на парність.
// Цей інтерфейс містить тільки 1 метод, який отримує ціле число і
// повертає результат логічного типу.
interface Odd {
  // Абстрактний метод перевірки на парність числа типу int
  boolean IsOdd(int d);
}

// Клас, в якому є метод, що знаходить суму елементів масиву
class SumArray {
  // Метод, що обчислює суму елементів масиву.
  // При обчисленні суми враховується результат повернення з лямбда-виразу.
  // Метод отримує 2 параметри:
  // - A - масив, що сумується;
  // - refLambda - посилання на функціональний інтерфейс Odd
  public int Sum(int[] A, Odd refLambda) {
    int sum=0;
    for (int i=0; i<A.length; i++)
      if (refLambda.IsOdd(A[i])) // чи парний елемент масиву?
        sum+=A[i];
    return sum;
  }
}

public class TrainLambda01 {

  public static void main(String[] args) {
    // Передача лямбда-виразу в метод як аргументу.
    // 1. Оголосити посилання на інтерфейс Odd
    Odd ref1;

    // 2. Присвоїти посиланню ref1 лямбда-вираз
    ref1 = (d) -> {
      // код перевірки на парність
      if (d%2==0) return true;
      else return false;
    };

    // 3. Продемонструвати виклик методу Sum класу SumArray та передачу йому лямбда-виразу
    // 3.1. Створити екземпляр класу SumArray
    SumArray obj1 = new SumArray();

    // 3.2. Оголосити деякий масив цілих чисел
    int[] A = { 5, 3, 8, 10, 2, 1, 2, 3 };

    // 3.3. Викликати метод обчислення суми елементів масиву
    //      з лямбда-виразом, що вибирає тільки парні елементи
    int sum = obj1.Sum(A, ref1);
    System.out.println("sum = " + sum);

    // 3.4. Передача в метод лямбда-виразу для непарних елементів
    sum = obj1.Sum(A,
      (Odd)((d) -> {
        if (d%2!=0) return true;
        else return false;
      }));
    System.out.println("sum = " + sum);
  }
}

Результат виконання програми

sum = 22
sum = 12

У вищенаведеному коді можна використати посилання на метод за зразком:

ім’я_екземпляру::ім’я_методу

У програмі передача методу IsOdd() в метод Sum() буде виглядати наступним чином:

// також працює - передається посилання на метод екземпляру
int sum = obj1.Sum(A, ref1::IsOdd);

 

5.3. Лямбда-вираз, що використовує шаблон (узагальнення)

Створити та викликати лямбда-вираз використовуючи шаблон (узагальнення). Лямбда-вираз повинен знаходити максимум з трьох чисел.

 

package TrainLambda;

// Узагальнений (шаблонний) функціональний інтерфейс.
// Містить метод, який отримує 3 числа типу T та
// повертає значення типу T.
interface Max3<T> {
  T Max(T a, T b, T c);
}

public class TrainLambda02 {

  public static void main(String[] args) {
    // Використання інтерфейсу Max3<T>
    // 1. Оголосити посилання на інтерфейс Max3<T>
    Max3<Integer> refInt;

    // 2. Створити лямбда-вираз з прив'язкою до типу int
    refInt = (a, b, c) -> {
      Integer max = a;
      if (max<b) max=b;
      if (max<c) max=c;
      return max;
    };

    // 3. Викликати лямбда вираз для трьох цілих чисел
    int res = refInt.Max(5, 7, 3); // res = 7
    System.out.println("res = " + res);

    // 4. Створити лямбда-вираз з прив'язкою до типу double
    Max3<Double> refDouble;
    refDouble = (a, b, c) -> {
      Double max = a;
      if (max<b) max=b;
      if (max<c) max=c;
      return max;
    };

    // 5. Викликати лямбда-вираз з прив'язкою до типу double
    double resMax = refDouble.Max(3.88, 2.55, 4.11);
    System.out.println("resMax = " + resMax);
  }
}

 

5.4. Розв’язок квадратного рівняння з допомогою лямбда-виразу

У задачі демонструється використання лямбда-виразу для розв’язку квадратного рівняння. Програма містить наступні складові:

  • функціональний інтерфейс Equation. В інтерфейсі оголошується один метод, що отримує три параметри типу double. Ці параметри є коефіцієнтами a, b, c квадратного рівняння. Метод повертає значення коренів рівняння у вигляді екземпляру класу Solution. Якщо рівняння не має розв’язку, то метод повертає null;
  • клас Solution – містить два поля x1, x2 типу double, які є коренями квадратного рівняння;
  • клас TrainLambda02, що містить функцію main(). У цій функції виконується побудова та використання лямбда-виразу для розв’язку квадратного рівняння для значень a=2, b=-8, c=4.

Текст програми наступний:

// Функціональний інтерфейс для розв'язку квадратного рівняння.
interface Equation {
  // Метод, що отримує параметри - коефіцієнти рівняння a*x^2 + b*x + c = 0.
  // Якщо рівняння не має розв'язку, то метод повертає null.
  // В іншому випадку, метод повертає екземпляр класу Solution
  // з коренями рівняння x1, x2.
  Solution CalcEquation(double a, double b, double c);
}

// Результат розв'язку квадратного рівняння.
class Solution {
  double x1;
  double x2;
}

public class TrainLambda02 {

  public static void main(String[] args) {
    // Лямбда-вираз, який розв'язує квадратне рівняння.
    // Результат повертається в екземплярі класу Solution.
    // 1. Оголосити посилання на функціональний інтерфейс
    Equation eq;

    // 2. Присвоїти посиланню лямбда-вираз, що розв'язує
    //    квадратне рівняння
    eq = (double a, double b, double c) -> {
      double d = b*b - 4*a*c; // дискримінант

      if (d>=0) {
        // Знайти корені
        Solution roots = new Solution();
        roots.x1 = (-b-Math.sqrt(d))/(2*a);
        roots.x2 = (-b+Math.sqrt(d))/(2*a);
        return roots;
      }
      else
        return null;
    };

    // 3. Розв'язати квадратне рівняння 2*x^2 - 8*x + 4 = 0
    Solution roots = eq.CalcEquation(2, -8, 4);
    if (roots==null)
      System.out.println("The solution has no roots.");
    else
    {
      System.out.println("x1 = " + roots.x1);
      System.out.println("x2 = " + roots.x2);
    }
  }
}

Результат виконання програми

x1 = 0.5857864376269049
x2 = 3.414213562373095

 


Зв’язані теми