• ,

Популярно о лямбдах в java. С примерами и задачами.

Для кого предназначена эта статья?
  • для тех, кто считает, что уже неплохо знает Java Core, но понятия не имеет о лямбдах в джаве. Ну может слышал там что-то про лямбды, но хз что это за ужасы такие.
  • для тех, кто имеет какое-то понимание о лямбдах, но до сих пор боится их использовать.

Если вы не входите в какую-то из этих категорий — возможно вам эта статья покажется скучной, некорректной и вообще не айс :) Можете либо смело проходить мимо, либо же предложить в комментах как эту статью можно улучшить/дополнить.

Эта статья не претендует на какую-либо академическую ценность и уж тем-более на новизну, а скорее даже наоборот: в ней я попытаюсь описать сложные (для кого-то) вещи как можно проще.

Я пишу эту статью потому, что просили объяснить про stream api, но я решил, что без понимания лямбд многие мои примеры в стримах будут непонятны. Вот и решил для начала сделать статью про лямбды.

Какие знания требуются для понимания этой статьи:
  1. Понимание объектно-ориентированного программирования (далее ООП), а именно:
    • знание что такое классы, объекты, какая между ними разница;
    • знание что такое интерфейсы, чем они отличаются от классов, какая между ними (интерфейсами и классами) связь;

    • знание что такое метод, как его вызвать, что такое абстрактный метод (или метод без реализации), что такое параметры/аргументы метода, как их туда передавать;
    • модификаторы доступа, статические методы/переменные, финальные методы/переменные;
    • наследование (классов, интерфейсов, множественное наследование интерфейсов).
  2. Знание Java Core: обобщенные типы(generics), коллекции (списки), потоки (threads).

Ну что ж, приступим.

Немного истории.
Лямбды пришли в джаву из функционального программирования, а туда, в свою очередь, из математических кругов. В середине 20го века в Америке в Принстонском университете был некий Алонзо Чёрч, который очень любил математику и всяческие абстракции. Именно Алонзо Чёрч и придумал лямбда-исчисление, которое скорее было просто набором неких абстрактных идей и никак не касалось программирования.
В то же время в том же Принстонском университете были и такие люди, как Алан Тьюринг и Джон фон Нейман. Паралельно с тем, как Чёрч придумал систему лямбда-исчисления, Тьюринг придумал свою систему, которая сейчас известна под названием «машина Тьюринга», которая очень удачно лягла на идею фон Неймана (и сейчас называется «архитектура фон Неймана») и в итоге мы имеем компьютеры, которые мы знаем.
Идеи же Алонзо Чёрча не стали такими же популярными. Но чуть позже некий Джон МакКарти (тоже выпускник Принстонского университета, но в тот момент сотрудник Массачусетского технологического института) заинтересовался идеями Чёрча и в 1958м создал первый функциональный язык программирования Lisp.
Ну а с выходом 8й джавы идеи функционального программирования и просочились в нее. Почти 70 лет, хех.

Суть.
Лямбда — это такая функция. Можете считать, что это как обычный метод в джаве, только его особенность в том, что его можно передавать в другие методы в качестве аргумента.
Да, стало возможным передавать в методы не только числа, строки и котиков, но и другие методы!
Когда нам это может понадобиться? Ну например когда мы хотим передать какой-нибудь callback. То есть хотим, чтоб тот метод, который мы вызываем, имел возможность вызвать какой-то другой метод, который мы ему передадим. То-есть, чтоб у нас была возможность в каких-то случаях передавать один callback, а в других — иной. И наш метод, который бы принимал наши callback-и — вызывал бы их.
Простой пример — сортировка. Допустим, мы пишем какую-то хитрую сортировку, которая выглядит примерно вот так:
public void mySuperSort() {
    // ... тут че-то делаем
    if(compare(obj1, obj2) > 0)
    // ... и тут че-то делаем
}

Там где if мы вызываем метод compare(), передаем туда два объекта, которые мы сравниваем, и хотим узнать какой из этих объектов «больше». Тот, который «больше» мы поставим перед тем, который «меньше». Я написал «больше» в кавычках, потому что мы пишем универсальный метод, который будет уметь сортировать не только по возрастанию, но и по убыванию (в таком случае «больше» будет тот объект, который по сути меньше, и наоборот).
Для того, чтобы задать вот это вот правило как мы хотим отсортировать — мы его должны как-то внутрь нашего метода mySuperSort() передать, если хотим, чтоб мы могли как-то «управлять» нашим методом во время того, когда мы его вызываем.
Да, можно было бы написать два отдельных метода mySuperSortAsc() и mySuperSortDesc() для сортировки по возрастанию или по убыванию, или же передавать какой-то параметр внутрь метода (допустим boolean какой-то и если там true — то сортировать по возрастанию, а если false — то по убыванию). Ну а что, если мы хотим отсортировать не какую-то простую структуру, а например список массивов строк? Как наш метод mySuperSort() будет знать по каким правилам сортировать те массивы строк? Просто по размеру, или по общей длине слов, или по алфавиту в зависимости от первой строки в массиве? А что если нам в каких-то случаях надо отсортировать этот список массивов по размеру массива, а в другом случае — по суммарной длине слов в массиве?
Я думаю, вы уже знаете про компараторы и про то, что в таких случаях мы просто передаем в наш метод сортировки объект компаратора, в котором мы и описываем правила, по которым мы хотим сортировать.
Поскольку стандартный метод sort() реализован по такому принципу как я описывал mySuperSort() — в примерах я буду использовать именно стандартный sort().

String[] array1 = {"мама", "мыла", "раму"};
String[] array2 = {"я", "очень", "люблю", "java"};
String[] array3 = {"мир", "труд", "май"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);


Результат:
  1. мама мыла раму
  2. мир труд май
  3. я очень люблю java
Тут массивы отсортированы по количеству слов в каждом массиве. Те, у кого слов меньше — те массивы «меньше» и потому стоят в начале, а тот массив, у кого слов больше — считается «больше» и потому оказался в конце.

Если в метод sort() мы передадим другой компаратор (sortByWordsLength) — то и результат будет другой:
  1. мир труд май
  2. мама мыла раму
  3. я очень люблю java
Теперь массивы отсортированы по общему количеству букв в словах такого массива. В первом случае 10 букв, во втором 12, и в третьем 15.

Если у нас используется только один компаратор, то мы можем не заводить под него отдельную переменную, а просто создать объект анонимного класса прямо в момент возова метода sort(). Примерно вот так:

String[] array1 = {"мама", "мыла", "раму"};
String[] array2 = {"я", "очень", "люблю", "java"};
String[] array3 = {"мир", "труд", "май"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});

Результат будет такой же, как и в первом случае.

Задача 1. Переписать этот пример так, чтобы он сортировал массивы не по возрастанию количества слов в массиве, а по убыванию.

Но все это мы уже знаем. Мы умеем передавать объекты в методы, мы можем передать тот или иной объект в метод в зависимости от того, что нам в данный момент надо, и внутри того метода, куда мы передаем такой объект — будет вызван тот метод, для которого мы написали реализацию. Возникает вопрос: так а при чем тут вообще лямбды?
При том, что лямбда — это и есть такой объект, который содержит ровно один метод. Такой себе объект-метод. Метод, запакованный в объект. Просто у них немного непривычный синтаксис (но об этом чуть позже).
Давайте еще раз взглянем на эту запись
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});

Тут мы берем наш список arrays и вызываем у него метод sort(), куда передаем объект компаратора с одним единственным методом compare() (хотя нам даже не важно как он там называется, ведь он — единственный в этом объекте, тут мы не промахнемся), который принимает два параметра, с которыми мы дальше и работаем.
Если вы работаете в идее, то наверняка видели, как она вам предлагает этот код значительно сократить.
arrays.sort((o1, o2) -> o1.length - o2.length);

Да, 6 строк переписали в одну короткую. Много чего пропало, но я гарантирую, что не пропало ничего важного и такой код будет работать абсолютно так же, как и при анонимном классе.

Задача 2. Попробовать догадаться как переписать решение задачи 1 через лямбду (в крайнем случае, попросите идею вам превратить ваш анонимный класс в лямбду).

Поговорим немного об интерфейсах.
В принципе, интерфейс — это просто список абстрактных методов. Когда мы создаем класс и говорим, что он будет имплементировать какой-то интерфейс — мы должны в нашем классе написать реализацию тех методов, которые перечислены в интерфейсе (ну или не писать, но сделать класс абстрактным).
Бывают интерфейсы со множеством разных методов (например List), бывают интерфейсы только с одним методом (например тот же Comparator или Runnable), бывают интерфейсы и вовсе без единого метода (так называемые интерфейсы-маркеры, например Serializable). Те интерфейсы, у которых только один метод называются еще функциональными интерфейсами. В 8й джаве они даже помечены специальной аннотацией @FunctionalInterface.
Именно интерфейсы с одним единственным методом и подходят для использования лямбдами. Как я уже говорил, лямбда — это метод, завернутый в объект. И когда мы передаем куда-то такой объект — мы по сути передаем этот один единственный метод. И поэтому получается, что нам даже не важно как этот метод называется. Все, что нам важно — это параметры, которые этот метод принимает, ну и собственно сам код метода.
Лямбда — это по сути реализация функционального интерфейса. Где видим интерфейс с одним методом — значит
такой анонимный класс можем переписать через лямбду. Если в интерфейсе больше/меньше одного метода — тогда нам лямбда не подойдет и будем использовать анонимный класс, или даже обычный.

Пришло время поковырять лямбды. :)

Синтаксис.
Общий синтаксис примерно такой:

(параметры) -> {тело метода}

То-есть, круглые скобки, внутри их параметры метода, «стрелочка» (это два символа подряд: минус и больше), после которой тело метода в фигурных скобках, как и всегда.
Параметры — соответствуют тем, которые указаны в интерфейсе при описании метода. Если тип переменных может быть четко определен компилятором (в нашем случае, точно известно, что мы работаем с массивами строк, потому что List — типизирован именно массивами строк) — то и тип переменных String[] можно не писать. Если не уверенны — пишите везде тип, а идея уже подсветит его серым, если он не нужен. Чуть подробней можно почитать в туториале оракла, например. Это называется «target typing».
Имена переменным можно дать какие угодно, не обязательно именно те, которые указаны в интерфейсе.
Если параметров нет — тогда просто круглые скобки. Если параметр только один — тогда просто имя переменной без круглых скобок можно писать.

С параметрами вроде разобрались, теперь про тело самой лямбды. Внутри фигурных скобок просто пишете код, как для обычного метода. Если у вас весь код состоит только из одной строки — можете вообще фигурных скобок и не писать (как и с if-ами, и с циклами).
Если же ваша лямбда что-то возвращает, но ее тело состоит из одной строки — писать return вовсе не обязательно. Но если у вас фигурные скобки — тогда как и в обычном методе обязательно писать return явно.

Примеры.
Пример 1.
() -> {}

Самый простой вариант. И самый бессмысленный :) Так как ничего не делает


Пример 2.
() -> ""

Тоже интересный вариант. Ничего не принимает и возвращает пустую строку (return опущен за ненадобностью). То же, но с ретурном:
() -> {
    return "";
}



Пример 3. Hello world на лямбдах
() -> System.out.println("Hello world!")

Ничего не принимает, ничего не возвращает (мы не можем поставить return перед вызовом System.out.println(), так как тип возвращаемого значения в методе println() — void), просто выводит на экран надпись. Идеально подходит для реализации интерфейса Runnable. Этот же пример более полный:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}

Ну или так:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}

Или даже можем сохранить лямбду как объект типа Runnable, а потом его уже передать в конструктор треда:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}

Рассмотрим чуть подробнее момент сохранения лямбды в переменную. Интерфейс Runnable нам говорит, что его объекты должны иметь метод public void run(). Как видим, метод run согласно интерфейсу ничего не принимает в качестве параметров. И ничего не возвращает (void). Поэтому при такой записи будет создан объект с каким-то методом, который ничего не принимает и не возвращает. Что вполне соответствует методу run() в интерфейсе Runnable. Поэтому мы и смогли поместить эту лямбду в переменную типа Runnable.


Пример 4.
() -> 42

Снова же, ничего не принимает, а возвращает число 42.
Такую лямбду можно поместить в переменную типа Callable, потому что в этом интерфейсе определен только один метод, который выглядит примерно так: V call(), где V — это тип возвращаемого значения (в нашем случае int). Соответственно, мы можем сохранить такую лямбду вот так:
Callable<Integer> c = () -> 42;



Пример 5. Лямбда в несколько строк.
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);    
}

И снова же, эта лямбда не имеет никаких параметров и тип возвращаемого значения у нее void (так как отсутствует return).


Пример 6.
x -> x

Тут мы принимаем что-то в переменную х, и ее же и возвращаем. Обратите внимание, что если принимается только один параметр — то скобки вокруг него можно не писать.
То же, но со скобками:
(x) -> x


А вот вариант с явным return
x -> {
    return x;
}


Ну или так, со скобками и ретурном
(x) -> {
    return x;
}


Ну или так, с явным указанием типа (и соответственно со скобками)
(int x) -> x



Пример 7.
x -> ++x

Принимаем х, возвращаем его же, но на 1 больше.
Можно переписать и так
x -> x + 1

В обоих случаях скобки вокруг параметра, тела метода и слово return не указываем, так как это не обязательно. Варианты со скобками и с ретурном описаны в примере 6.


Пример 8.
(x, y) -> x % y

Принимаем какие-то х и у, а возвращаем остаток от деления икса на игрек. Скобки вокруг параметров тут уже обязательны. Необязательны они только когда параметр всего один.
Вот так с явным указанием типов:
(double x, int y) -> x % y



Пример 9.
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}

Принимаем объект кота, строку с именем и целое число возраст. В самом методе устанавливаем коту переданные имя и возраст. Поскольку переменная cat у нас ссылочного типа — то и объект кота вне лямбды изменится (получит переданные внутрь имя и возраст).

Немного усложненный вариант, где используется подобная лямбда:
public class Main {
    public static void main(String[] args) {
        // создаем кота и выводим на экран чтоб убедиться, что он "пустой"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // создаем лямбду
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // вызываем метод, в который передаем кота и лямбду
        changeEntity(myCat, s);
        // выводим на экран и видим, что состояние кота изменилось (имеет имя и возраст)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Мурзик", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}


Результат:
  1. Cat{name='null', age=0}
  2. Cat{name='Мурзик', age=3}
Как видно, сначала объект кота имел одно состояние, а после использования лямбды состояние изменилось.

Лямбды успешно ложатся на дженерики. И если нам понадобится создать класс собаки, например, который тоже будет имплементить WithNameAndAge, то в методе main() мы можем те же операции проделать и с собакой, абсолютно не меняя саму лямбду.

Задача 3. Написать функциональный интерфейс с методом, который принимает число и возвращает булевое значение. Написать реализацию такого интерфейса в виде лямбды, которая возвращает true если переданное число делится без остатка на 13.

Задача 4. Написать функциональный интерфейс с методом, который принимает две строки и возвращает тоже строку. Написать реализацию такого интерфейса в виде лямбды, которая возвращает ту строку, которая длиннее.

Задача 5. Написать функциональный интерфейс с методом, который принимает три дробных числа: a, b, c и возвращает тоже дробное число. Написать реализацию такого интерфейса в виде лямбды, которая возвращает дискриминант. Кто забыл, D = b^2 — 4ac.

Задача 6. Используя функциональный интерфейс из задачи 5 написать такую лямбду, которая возвращает результат операции a * b^c.

Доступ к внешним переменным.
Скомпилируется ли такой код с анонимным классом?
int counter = 0;
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter++;
    }
};

Нет. Переменная counter должна быть final. Ну или не обязательно final, но в любом случае изменять свое значение она не может.
Тот же принцип используется и в лямбдах. Лямбда имеет доступ ко всем переменным, которые ей «видны» из того места, где она объявлена. Но она не должна их изменять (присваивать новое значение).
Но есть вариант обхода этого ограничения в анонимных классах. Достаточно лишь создать переменную ссылочного типа и менять внутреннее состояние объекта, при этом сама переменная все так же будет указывать на тот же объект, и в таком случае можно смело указывать ее как final.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter.incrementAndGet();
    }
};

Здесь у нас переменная counter является ссылкой на объект типа AtomicInteger. А для изменения состояния этого объекта используется метод incrementAndGet(). Значение самой переменной во время работы программы не меняется и всегда указывает на один и тот же объект, что позволяет нам объявить переменную сразу с ключевым словом final.

Эти же примеры, но с лямбдами.
int counter = 0;
Runnable r = () -> counter++;

Не скомпилируется по той же причине, что и такой же вариант с анонимным классом: counter не должна меняться во время работы программы.
Зато вот так — все норм:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();


Причем это касается и вызова методов. Изнутри лямбда-выражения можно не только обращаться ко всем «видимым» переменным, но и вызывать те методы, к которым есть доступ.
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {
        System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}

Хотя метод staticMethod() и приватный, но он «доступен» для вызова внутри метода main(), поэтому точно так же доступен для вызова изнутри лямбды, которая создана в методе main.

Момент выполнения кода лямбды.
Возможно, для многих этот вопрос покажется слишком простым, но: когда выполнится код внутри лямбды? В момент создания? Или же в тот момент, когда (еще и неизвестно где) она будет вызвана?
Проверить довольно просто.
System.out.println("Запуск программы");

// много всякого разного кода
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого кода
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();


Вывод на экран:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!

Видно, что код лямбды выполнился в самом конце, после того, как был создан тред и лишь когда процесс выполнения программы дошел до фактического выполнения метода run(). А не в момент ее объявления. Объявив лямбду мы лишь создали объект типа Runnable и описали поведение его метода run(). Сам же метод был запущен значительно позже.

Method References (Ссылки на методы)
Не имеет прямого отношения к самим лямбдам, но я считаю, что будет логично сказать об этом пару слов в этой статье.

Допустим у нас есть лямбда, которая не делает ничего особенного, а просто вызывает какой-то метод и все.
x -> System.out.println(x)

Ей передали какой-то х, а она просто вызвала System.out.println() и передала туда этот х.
В таком случае, мы можем ее заменить просто на ссылку на нужный нам метод. Вот так:
System.out::println

Да, без скобок в конце.

Более полный пример:
List<String> strings = new LinkedList<>();
strings.add("мама");
strings.add("мыла");
strings.add("раму");
        
strings.forEach(x -> System.out.println(x));

В последней строке мы используем метод forEach(), который принимает объект интерфейса Consumer. Это снова же функциональный интерфейс, у которого только один метод void accept(T t). Соответственно, мы пишем лямбду, которая принимает один параметр (поскольку он типизирован в самом интерфейсе — то тип параметра мы не указываем, а просто указываем, что называться он у нас будет х). Ну а в самом теле лямбды пишем код, который будет выполняться когда будет вызван метод accept(). В нашем случае, мы просто выводим на экран то, что попало в переменную х. Сам же метод forEach() просто ходит по всем элементам коллекции, вызывает у переданого ему объекта интерфейса Consumer (нашей лямбды) метод accept(), куда и передает каждый элемент из коллекции.
Как я уже сказал, такую лямбду (которая просто вызывает другой метод и все) мы можем заменить ссылкой на нужный нам метод. Тогда наш код будет выглядеть так:
List<String> strings = new LinkedList<>();
strings.add("мама");
strings.add("мыла");
strings.add("раму");

strings.forEach(System.out::println);


Главное тут чтоб совпадали принимаемые параметры методов (println() и accept()). Поскольку метод println() может принимать что угодно (он перегружен для всех примитивов и для любых объектов) — мы и можем вместо лямбды передать в forEach() просто ссылку на метод println(). Тогда forEach() будет брать каждый элемент коллекции и передавать его напрямую в метод println();

Кто сталкивается с этим впервые — прошу обратить внимание, что мы не вызываем метод System.out.println() (с точками между словами и со скобочками в конце), а именно передаем саму ссылку на этот метод.
При такой записи
strings.forEach(System.out.println());

у нас будет ошибка компиляции. Так как перед вызовом forEach() джава увидит, что вызывается System.out.println(), поймет, что возвращается void и будет пытаться этот void передать в forEach(), который там ждет объект типа Consumer.

Синтаксис использования Method References довольно простой.
1. Передаем ссылку на статический метод
ИмяКласса:: имяСтатическогоМетода
public class Main {
    public static void main(String[] args) {
        List<String> strings = new LinkedList<>();
        strings.add("мама");
        strings.add("мыла");
        strings.add("раму");

        strings.forEach(Main::staticMethod);
    }

    private static void staticMethod(String s) {
        // do something
    }
}


2. Передаем ссылку на не статический метод используя существующий объект
имяПеременнойСОбъектом:: имяМетода
public class Main {
    public static void main(String[] args) {
        List<String> strings = new LinkedList<>();
        strings.add("мама");
        strings.add("мыла");
        strings.add("раму");

        Main instance = new Main();
        strings.forEach(instance::nonStaticMethod);
    }

    private void nonStaticMethod(String s) {
        // do something
    }
}


3. Передаем ссылку на не статический метод используя класс, в котором реализован такой метод
ИмяКласса:: имяМетода
public class Main {
    public static void main(String[] args) {
        List<User> users = new LinkedList<>();
        users.add(new User("Вася"));
        users.add(new User("Коля"));
        users.add(new User("Петя"));

        users.forEach(User::print);
    }

    private static class User {
        private String name;

        private User(String name) {
            this.name = name;
        }

        private void print() {
            System.out.println(name);
        }
    }
}


4. Передаем ссылку на конструктор
ИмяКласса::new

Использование ссылок на методы очень удобно, когда у нас есть готовый хороший и полностью устраивающий нас метод, который бы мы хотели использовать в качестве callback-а. В таком случае, вместо того чтобы писать лямбду с кодом того метода, или же лямбду, где мы просто вызываем этот устраивающий нас метод — мы просто передаем ссылку на этот метод и все.

Интересное отличие между анонимным классом и лямбдой.
В анонимном классе ключевое слово this указывает на объект этого анонимного класса. А вот используя this внутри лямбды — мы получаем доступ к объекту обрамляющего класса, того, где мы эту лямбду собственно и написали. Так происходит потому, что лямбды при компиляции превращаются в приватный метод того класса, где они написаны.
Хотя использовать эту фичу лично я не рекомендовал бы, так как это создает побочный эффект (side effect), что противоречит принципам функционального программирования, зато вполне соответствует ООП. ;)

Откуда я брал информацию или что почитать еще:
Туториал на официальном сайте оракла. Много, детально, с примерами, но на английском.
Тот же ораклавский туториал, но глава именно про Method References.
Статья на хабре про функциональное программирование (перевод другой статьи). Многобуков и про лямбды очень мало, так как там о функциональном программировании в общем.
Любителям позалипать на википедии.
Ну и, конечно же, кучу всего находил в гугле :)
Картинку стащил отсюда

4 комментария

lichMax
А где вторая часть статьи? Что-то не могу её найти на джавараш (надо было всё же сюда выставить). А так, статья интересная. Примеры хорошие, понятные.
lichMax
А всё понял. На просто джавараш Вы разбили её на две части. Хорошо, а где тогда статья про стримы?
fatfaggy
да я там думал просто описать что это такое, ну и рассказать про foreach, filter, map и reduce, так как именно они чаще всего и используются, как мне кажется)
но потом подумал, что этого как-то мало для статьи… в общем, пока так за нее и не садился))
lichMax
Жаль. Эта статья хороша помогла въехать в лямбды.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.