Глубокое погружение в лямбда-выражения на Java

Глубокое погружение в лямбда-выражения на Java

Эй, там! Это руководство полностью посвящено освоению лямбда-выражений на Java. Вы узнаете всё, что вам нужно знать, от основ создания и работы до более продвинутых тем, таких как функциональные интерфейсы и ссылки на методы. Независимо от того, новичок вы или опытный разработчик, это руководство поможет вам повысить уровень вашей лямбда-игры. Итак, давайте погрузимся в мир функционального программирования с помощью Java-лямбд!

Бонус: В конце статьи есть несколько практических вопросов, которые помогут вам подготовиться к следующему собеседованию и оценить свои знания.

TL; DR: Лямбда-выражения - это мощный инструмент для Java-разработчиков, который включает парадигмы функционального программирования и предоставляет способ работы с коллекциями и потоками в Java. Освоив лямбда-выражения, вы, как разработчик, будете писать лучший код, работать эффективнее и создавать более удобные в обслуживании и масштабируемые приложения.

Содержание

  • Введение в лямбда-выражения
  • Базовый синтаксис лямбда-выражений
  • Функциональные интерфейсы на Java
  • Работа с коллекцией с использованием лямбда-выражений
  • Ссылки на методы и конструкторы.
  • Лучшие практики и советы по работе с лямбда-выражениями в Java
  • Вопросы для собеседований
  • Заключение

Введение в лямбда-выражения

Лямбда-выражение было впервые представлено в Java 8 как новая мощная функция, которая позволяет разработчикам писать короткие анонимные функции, заменяющие более подробные определения функций. Они предоставляют способ сделать код более кратким и выразительным, позволяя разработчикам выражать то, что они хотят делать, а не то, как они хотят это делать.

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

Базовый синтаксис лямбда-выражений

Лямбда-выражения состоят из 3 частей: списка параметров, стрелки (->) и тела, которое может быть выражением или блоком кода. Список параметров задаёт входные данные для функции, в то время как стрелка отделяет список параметров от тела. Тело - это код, который выполняет логику функции.

(x,y)->x+y;

Стрелка отделяет список параметров от основного текста. Это можно рассматривать как выражение ”сопоставляется“ или "становится”.

Пример 1:

((x, y) -> x + y можно прочитать как "x и y сопоставляются с x плюс y".

Тело - это код, который выполняет логику функции. Это может быть выражение или блок кода. Если тело представляет собой одно выражение, для него не требуются фигурные скобки.

Пример 2:

x -> x * x - это лямбда-выражение с одним телом выражения, которое возвращает квадрат входных данных.

Если тело содержит несколько операторов, оно должно быть заключено в фигурные скобки.

Пример 3:

(x, y) -> { int sum = x + y; return sum; } - это лямбда-выражение с телом блока, которое возвращает сумму входных данных.

Функциональные интерфейсы на Java

Лямбда-выражения работают с функциональными интерфейсами, которые представляют собой интерфейсы, имеющие один абстрактный метод. Они используются для представления сигнатуры лямбда-выражения. Функциональные интерфейсы могут быть определены разработчиком или найдены в библиотеке Java.

//filename: MyInterface.java @FunctionalInterface interface MyInterface { void doSomething(String input); } //filename: MyClass.java public class MyClass { public static void main(String[] args) { MyInterface myLambda = (String input) -> System.out.println("Input: " + input); myLambda.doSomething("Hello World!"); // Output: Input: Hello World! } }

В этом примере MyInterface является функциональным интерфейсом с единственным методом doSomething, который принимает параметр String и возвращает void. Метод main создаёт лямбда-выражение, которое реализует метод doSomething, а затем вызывает метод со строкой "Hello World!" в качестве аргумента.

Вывод типа в лямбда-выражениях

Вывод типа - это функция в Java, которая позволяет компилятору определять тип параметров лямбда-выражения. Это означает, что разработчику не нужно явно указывать тип параметра. Компилятор определяет тип параметра на основе контекста, в котором используется лямбда-выражение.

//filename: MyInterface.java @FunctionalInterface interface MyInterface { int doSomething(int x, int y); } //filename: MyClass.java public class MyClass { public static void main(String[] args) { MyInterface myLambda = (x, y) -> x + y; int result = myLambda.doSomething(3, 5); System.out.println(result); // Output: 8 } }

В этом примере MyInterface - это функциональный интерфейс с одним методом doSomething, который принимает два параметра int и возвращает значение int. Метод main создаёт лямбда-выражение, реализующее метод doSomething, и присваивает его переменной myLambda. Обратите внимание, что типы параметров не указаны в лямбда-выражении, поскольку для их определения используется вывод. Лямбда-выражение просто добавляет два входных параметра и возвращает результат, который затем выводится на консоль.

В заключение, функциональные интерфейсы и вывод типов - это две важные концепции в Java, которые работают вместе для обеспечения возможности лямбда-выражений. Функциональные интерфейсы обеспечивают сигнатуру лямбда-выражения, в то время как вывод типов позволяет разработчику писать более краткий и выразительный код.

Работа с коллекцией с использованием лямбда-выражений

Лямбда-выражения и потоки предоставляют мощный инструмент для обработки коллекций в Java. Потоки представляют собой последовательность элементов, которые могут обрабатываться параллельно или последовательно, а лямбда-выражения используются для указания операций, которые должны выполняться над каждым элементом в потоке. Вот несколько примеров того, как использовать лямбда-выражения и потоки для обработки коллекций:

Пример 1: Фильтрация списка

Предположим, у нас есть список целых чисел, и мы хотим отфильтровать все чётные числа. Мы можем сделать это, используя поток и лямбда-выражение следующим образом:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> filteredNumbers = numbers.stream() .filter(n -> n % 2 == 1) .collect(Collectors.toList()); System.out.println(filteredNumbers); // Output: [1, 3, 5, 7, 9]

В этом примере мы используем метод stream() для создания потока из списка numbers. Затем мы используем метод filter(), чтобы применить лямбда-выражение, которое отфильтровывает все чётные числа (т.е. n % 2 == 1 означает, что число нечётное). Наконец, мы используем метод collect(), чтобы преобразовать отфильтрованный поток обратно в список.

Пример 2: Сопоставление списка

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

List<String> strings = Arrays.asList("hello", "world", "java"); List<String> upperCaseStrings = strings.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println(upperCaseStrings); // Output: [HELLO, WORLD,JAVA]

В этом примере мы используем метод stream() для создания потока из списка strings. Затем мы используем метод map(), чтобы применить лямбда-выражение, которое преобразует каждую строку в верхний регистр. Ссылка на метод String::toUpperCase используется для представления лямбда-выражения, которое принимает строку и возвращает её версию в верхнем регистре. Наконец, мы используем метод collect(), чтобы преобразовать отображённый поток обратно в список.

Пример 3: Сокращение списка

Предположим, у нас есть список целых чисел, и мы хотим вычислить сумму всех чисел. Мы можем сделать это, используя поток и лямбда-выражение следующим образом:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, (a, b) -> a + b); System.out.println(sum); // Output: 15

В этом примере мы используем метод stream() для создания потока из списка numbers. Затем мы используем метод reduce(), чтобы применить лямбда-выражение, которое суммирует все числа. Лямбда-выражение принимает два целых числа (a и b) и возвращает их сумму. Метод reduce() принимает начальное значение 0 и применяет лямбда-выражение к каждому элементу в потоке для вычисления конечной суммы.

Это всего лишь несколько примеров того, как использовать лямбда-выражения и потоки для обработки коллекций в Java.

Ссылки на методы и конструкторы.

Ссылки на методы - это сокращённое обозначение для вызова метода в объекте или классе. Они позволяют нам упростить код, который в противном случае потребовал бы определения лямбда-выражения. Существует четыре типа ссылок на методы: ссылки на статические методы, ссылки на методы экземпляра, ссылки на конструкторы и ссылки на конструкторы массива.

1. Ссылки на статические методы: Ссылки на статические методы используются для вызова статических методов в классе. Они определяются с использованием синтаксиса className::methodName. Вот пример:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Joe"); names.stream() .map(String::toUpperCase) .forEach(System.out::println);

В этом примере мы используем операцию сопоставления для преобразования каждого имени в верхний регистр с помощью метода toUpperCase. Затем мы используем операцию forEach для вывода каждого имени в верхнем регистре в консоль с помощью метода println.

2. Ссылки на методы экземпляра: Ссылки на методы экземпляра используются для вызова методов экземпляра объекта. Они определяются с использованием синтаксиса objectName::methodName. Вот пример:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Joe"); names.stream() .map(String::length) .forEach(System.out::println);

В этом примере мы используем операцию сопоставления, чтобы получить длину каждого имени, используя метод length. Затем мы используем операцию forEach для вывода каждой длины в консоль.

3. Ссылки на конструктор: Ссылки на конструктор используются для создания новых экземпляров класса. Они определяются с использованием синтаксиса className::new. Вот пример:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Joe"); List<Person> people = names.stream() .map(Person::new) .collect(Collectors.toList());

В этом примере мы используем операцию map для создания нового объекта Person для каждого имени в списке с помощью конструктора Person. Затем мы собираем результирующие объекты Person в новый список, используя сборщик ToList.

4 . Ссылки на конструктор массива: Ссылки на конструктор массива используются для создания новых массивов. Они определяются с использованием синтаксиса Type[]::new. Вот пример:

IntStream.range(0, 5) .mapToObj(int[]::new) .forEach(System.out::println);

В этом примере мы используем операцию mapToObj для создания нового целочисленного массива с длиной 5, используя ссылку на конструктор int[]. Затем мы используем операцию forEach для вывода каждого массива в консоль.

Лучшие практики и советы по работе с лямбда-выражениями в Java

1. Сохраняйте лямбда-выражения короткими и простыми: Одно из основных преимуществ лямбда-выражений заключается в том, что они позволяют создавать более сжатый код. Однако важно не злоупотреблять ими и не делать их слишком сложными. Делайте лямбда-выражения короткими и простыми для поддержания удобочитаемости кода. Вот пример:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Joe"); // Don't do this names.stream() .filter(name -> name.startsWith("A") || name.startsWith("B") || name.startsWith("C") || name.startsWith("D")) .forEach(System.out::println); // Do this instead names.stream() .filter(name -> "ABCD".contains(name.substring(0,1))) .forEach(System.out::println);

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

2. Используйте вывод типа: Вывод типа - это функция в Java, которая позволяет компилятору определять тип лямбда-выражения. Это делает код более кратким и лёгким для чтения. Вот пример:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Joe"); // Without type inference names.stream() .map((String name) -> name.toUpperCase()) .forEach(System.out::println); // With type inference names.stream() .map(name -> name.toUpperCase()) .forEach(System.out::println);

В этом примере второе лямбда-выражение использует вывод типа для удаления явного объявления типа. Это делает код более лёгким для чтения и менее подробным.

3. Используйте ссылки на методы, когда это уместно: Ссылки на методы - это сокращённое обозначение для вызова метода в объекте или классе. Они могут упростить код, который в противном случае потребовал бы определения лямбда-выражения. Используйте ссылки на методы когда это уместно, чтобы сделать код более кратким и читабельным. Вот пример:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Joe"); // Without method references names.stream() .map(name -> name.length()) .forEach(length -> System.out.println("Length: " + length)); // With method references names.stream() .map(String::length) .forEach(length -> System.out.println("Length: " + length));

В этом примере второе лямбда-выражение использует ссылку на метод для вызова метода length для каждого объекта String в списке. Это делает код более кратким и лёгким для чтения.

4. Будьте осторожны с операциями с сохранением состояния: операции с сохранением состояния, такие как distinct и sorted, могут привести к неожиданным результатам при использовании с параллельными потоками. Будьте осторожны при использовании этих операций и помните об их ограничениях. Вот пример:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Joe"); // Without parallel stream names.stream() .distinct() .sorted() .forEach(System.out::println); // With parallel stream names.parallelStream() .distinct() .sorted() .forEach(System.out::println);

В этом примере второе лямбда-выражение использует параллельный поток для обработки списка. Однако операция distinct может не дать ожидаемых результатов, поскольку она зависит от поведения с сохранением состояния, которое может быть нарушено параллельной обработкой.

5. Используйте лямбда-выражения для реализации функциональных интерфейсов: Лямбда-выражения можно использовать для реализации функциональных интерфейсов, которые представляют собой интерфейсы, определяющие один абстрактный метод. Это может быть мощным инструментом для написания краткого и выразительного кода.

6. Используйте лямбда-выражения с коллекциями: Лямбда-выражения особенно полезны при работе с коллекциями, поскольку они могут упростить обычные операции, такие как фильтрация, сопоставление и сокращение. Вот пример:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Filtering using a Lambda expression List<Integer> evenNumbers = numbers.stream() .filter(number -> number % 2 == 0) .collect(Collectors.toList()); // Mapping using a Lambda expression List<Integer> doubledNumbers = numbers.stream() .map(number -> number * 2) .collect(Collectors.toList()); // Reducing using a Lambda expression int sum = numbers.stream() .reduce(0, (acc, number) -> acc + number);

В этом примере первое лямбда-выражение отфильтровывает все нечётные числа из списка, второе удваивает все числа в списке, а третье вычисляет сумму всех чисел в списке.

7. Избегайте побочных эффектов: Лямбда-выражения должны быть свободны от побочных эффектов, что означает, что они не должны изменять какое-либо внешнее состояние или иметь какие-либо другие непреднамеренные последствия. Это помогает сохранить код предсказуемым и ремонтопригодным. Вот пример:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Don't do this - Lambda expression has a side effect int sum = 0; numbers.forEach(number -> sum += number); // Do this instead - Use a stream and the reduce operation int sum = numbers.stream() .reduce(0, (acc, number) -> acc + number);

В этом примере первое лямбда-выражение изменяет внешнюю переменную sum, что является побочным эффектом. Во втором примере этого удаётся избежать, используя операцию уменьшения для вычисления суммы.

8. Учитывайте производительность: Хотя лямбда-выражения могут упростить код, они также могут влиять на производительность. В целом, лямбда-выражения работают медленнее, чем традиционный Java-код, поэтому при их использовании важно учитывать производительность. Вот пример:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // Without a Lambda expression for (int i = 0; i < numbers.size(); i++) { System.out.println(numbers.get(i)); } // With a Lambda expression numbers.forEach(System.out::println);

В этом примере второе лямбда-выражение является более кратким, но оно может быть медленнее, чем традиционный цикл Java, из-за накладных расходов на создание и вызов лямбда-выражения.

9. Используйте поддержку IDE: Современные Java IDE, такие как IntelliJ IDEA и Eclipse, обеспечивают отличную поддержку для работы с лямбда-выражениями. Они могут помочь с завершением кода, рефакторингом и отладкой, упрощая написание и поддержку кода на основе лямбда.

Вопросы для собеседований

  • Как можно использовать ссылки на методы для упрощения следующего лямбда-выражения: (String s) -> System.out.println(s)?
  • Как следующее лямбда-выражение изменяет состояние внешней переменной и что можно было бы сделать, чтобы этого не произошло?
int count = 0; List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.forEach(n -> { count += n; });
  • Напишите лямбда-выражение, которое принимает список строк и возвращает новый список со всеми строками в верхнем регистре.
  • Как использование лямбда-выражений может повысить производительность крупномасштабного приложения для обработки данных и каковы некоторые рекомендации по достижению этого?
  • Можете ли вы привести пример пользовательского функционального интерфейса, который принимает два аргумента, и продемонстрировать, как его можно использовать с лямбда-выражением?
  • Как использование лямбда-выражений может улучшить читаемость и ремонтопригодность приложения, и каковы некоторые рекомендации для достижения этого?
  • Напишите лямбда-выражение, которое принимает отображение строковых ключей и целых значений и возвращает новое отображение со всеми ключами в верхнем регистре и значениями, умноженными на 2.
  • Как использование потоков в сочетании с лямбда-выражениями может повысить производительность задач обработки данных в Java? Приведите пример того, как этого можно достичь.
  • Можно ли использовать лямбда-выражения с аннотациями в Java? Если да, то как это можно сделать?
  • Можно ли использовать лямбда-выражения с не конечными локальными переменными? Если да, то каковы последствия?

Заключение

Подводя итог, можно сказать, что лямбда-выражения - это мощный инструмент в Java, который способствует сжатию и сопровождаемости кода. Мы рассмотрели базовый синтаксис и продемонстрировали, как использовать лямбды с коллекциями, а также ссылки на методы и конструкторы для улучшения качества кода. Следуя лучшим практикам, таким как написание краткого и тестируемого кода и избежание усложнения кода, разработчики могут использовать лямбды для создания эффективного и ремонтопригодного кода.

Спасибо за чтение!

Статья была взята из этого источника:

2
Начать дискуссию