Лямбда-вирази у Java

Що таке лямбда-вираз?

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

Я думаю, якщо ви все таки тицьнули на цей пост, то принаймні вухом чули, що таке лямбда-вирази. А якщо не чули, то я з вами швидко поділюсь.

Лямбда-вираз (також відомий як анонімна функція, лямбда-функція або анонімний вираз) - це спосіб створення функцій в багатьох програмувальних мовах програмування. Він отримав назву "лямбда" через лямбда-числення, математичну теорію функцій і їх визначень.

Тобто це спосіб запису функції без її імені. Ось приклад синтаксису в деяких мовах:

Java

(a) -> {a + 10}; 

Python

lambda a : a + 10

Haskell

\a -> a + 10

Ідея лямбда-виразів походить з математики, напевно як і всіх концепцій у програмуванні, але про математику поговоримо в іншій частині. Цього разу тільки про Джаву.

Реалізація лямбда-виразів в Джаві

В Джаві лямбда-вирази з’явились тільки у восьмій версії. До того часу все писалось на анонімних класах.

Анонімний клас (Anonymous class) - це концепція в Java, яка дозволяє створювати класи без вказання їх імені. Анонімні класи зазвичай використовуються для реалізації інтерфейсів або класів-абстракцій на льоту, без необхідності створювати окремий файл для цього класу.

Наприклад:

public class Main {
    public static void main(String[] args) {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                
            }
        };

        myRunnable.run();
    }
}

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

Під капотом JVM створює новий клас, який імплементує інтерфейс і вже з нього створює реалізований об’єкт. Лямбда-вирази це майже синтаксичний цукор, як і ананомний клас.

Майже, бо JVM для лямбда-виразів використовує байт-код інструкцію invokedynamic, яку не використовує для анонімних класів, що не дозволяє зв’язати лямбда-вирази і анонімні класи, як одну сутність.

Java компілятор генерує байткод, яким користується invokedynamic під капотом для реалізації функціонального програмування та лямбда-виразів. invokedynamic допомагає оптимізувати виклик функцій і підтримує зв'язування функцій на етапі виконання, що дозволяє підтримувати динамічні мови та функціональні конструкції.

invokedynamic дозволяє генерувати код під час виконання.

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

public class Main {
    public static void main(String[] args) {
        Runnable myRunnable = () -> {};
        myRunnable.run();
    }
}

ООП vs Функційне програмування

Лямбда-вираз, навідмінно від анонімних класів, дозволяє перевизначати тільки інтерфейси в яких один метод. Чому тільки для інтерфейсів з одним методом? Бо лямбда визначає тільки одну функцію по своїй природі і в неї немає імені, щоб вона могла ідентифікувати, яку саме абстрактну функцію в інтерфейсі вона перевизначає.

По своїй природі лямбда повинна представляти одну функцію, в Джаві не можна описати функцію окремо від стану. Тому використовують функційні інтерфейси.

Інтерфейс, в концепції, служить абстрактним станом для функції, щоб задовільняти правило Джави щодо станів. Таким чинов лямбда-вираз завжди можна приписати до якогось інтерфейсу. А “функційним“ його робить анотація @FunctionalInterface та наявність однієї функції з якою ніколи не сплутаєш.

Ця прив’язка до стану має свій відчутний наслідок.

Лямбда-вирази відчуваються в джаві дивно, бо це ООП дуже хоче налізти на наш гарний функційний світ.

Ідентифікатором для лямбди мали би бути вхідні і вихідні дані, але в нас тут сильний запах ООП, який під капотом ще прив’язує кожну лямбду до типу який вона визначає, тобто, якщо метод приймає Runnable, то джава сприймає вхідний вираз, як реалізацію Runnable, а не як анонімну функцію. Більш детально:

@FunctionalInterface
public interface UnaryInterface {
    void doSomething();
}
public class Main {
    public static void main(String[] args) {
        UnaryInterface unaryInterface = () -> {};
        Runnable runnable = () -> {};

        // Буде працювати
        acceptFunction(() -> {});
    }
}

public void acceptFunction(Runnable runnable) {}

Але якщо ми передаємо вже визначенні інтерфейси, то звісно програма не працюватиме.

public class Main {
    public static void main(String[] args) {
        UnaryInterface unaryInterface = () -> {};
        Runnable runnable = () -> {};
        
        // Не буде працювати
        acceptFunction(unaryInterface);
    }
}

public void acceptFunction(Runnable runnable) {}
Якби анонімну функцію дійсно визначали вхідні і вихідні дані, то UnaryInterface та Runnable по своїй суті і використанню мали би бути однаковими функціями, але у нас тут ООП, яке чітко каже, що UnaryInterface і Runnable є різними типами. Зрештою, важко з ним сперечатись.

Імплементація лямбд в Джаві повністю підкоряється філософії мови, що я цілком приймаю, як Джава розробник, але вважаю, що також необхідно бачити, яким чином нові концепції приносяться в мову та як ці концепції виглядають в первинній формі.

Додаткові матеріали:

https://wiki.jvmlangsummit.com/images/1/1e/2011_Goetz_Lambda.pdf
https://stackoverflow.com/questions/30002380/why-are-java-8-lambdas-invoked-using-invokedynamic

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Yaroslav Kutsela
Yaroslav Kutsela@penrose

Java Software Engineer

7KПрочитань
1Автори
83Читачі
На Друкарні з 26 квітня

Більше від автора

  • Stack та Heap

    В JVM використовуються дві структури для зберігання інформації в пам’яті: Stack та Heap. Вони мають полярну філософію і ми не можемо обійтись без жодної із них. У цьому пості я намагатимусь обширно опрацювати причини використання обох структур та їхні особливості.

    Теми цього довгочиту:

    Java
  • Рівні ізоляції транзакцій у БД

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

    Теми цього довгочиту:

    Бази Даних
  • Функціональна залежність у БД

    Пост про функціональну залежність в реляційних множинах. Визначення. Повторення значень в атрибуті. Приклад з п'ятьма атрибутами. Тривіальна залежність. Замикання. залежностей та атрибутів. Незвідні множини. Використання

    Теми цього довгочиту:

    Програмування

Вам також сподобається

  • Будемо робити застосунок для обліку фінансів

    Знайома вам ситуація, коли кошти, які мали бути витрачені на велику покупку якимось чином зникали до моменту цієї самої покупки? Болісний момент, який може стати тригером, що приведе вас до кращого життя. Теоретично. Але щоб це сталося, потрібно якось вести історію витрат.

    Теми цього довгочиту:

    Розробка
  • Створення Mash Script: виклики у розробці інтерпретатора

    Mash Script - це динамічно типізована мова програмування, інтерпретатор якої написаний на мові Python. Вона має можливість "псевдо-компіляції", що дозволяє упаковувати програму з інтерпретатором у .exe файли.

    Теми цього довгочиту:

    Програмування

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається

  • Будемо робити застосунок для обліку фінансів

    Знайома вам ситуація, коли кошти, які мали бути витрачені на велику покупку якимось чином зникали до моменту цієї самої покупки? Болісний момент, який може стати тригером, що приведе вас до кращого життя. Теоретично. Але щоб це сталося, потрібно якось вести історію витрат.

    Теми цього довгочиту:

    Розробка
  • Створення Mash Script: виклики у розробці інтерпретатора

    Mash Script - це динамічно типізована мова програмування, інтерпретатор якої написаний на мові Python. Вона має можливість "псевдо-компіляції", що дозволяє упаковувати програму з інтерпретатором у .exe файли.

    Теми цього довгочиту:

    Програмування