Лямбда-вирази у 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

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

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

  • Stack та Heap

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

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

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

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

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

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

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

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

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

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

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

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

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