Одна из проблем с анонимными внутренними классами заключается в том, что анонимный внутренний класс имеет очень простую реализацию, такую как интерфейс.только одна абстрактная функция, то синтаксис анонимных внутренних классов немного неуклюжий и неясный. У нас часто возникает реальная необходимость передать функцию в качестве параметра другой функции, например, когда кнопка нажата, нам нужно установить функцию ответа кнопки для объекта кнопки. Лямбда-выражения могут принимать функции в качестве параметров функций и код (функции) в качестве данных (формальных параметров), что соответствует вышеуказанным требованиям. При реализации интерфейса только с одной абстрактной функцией использование лямбда-выражений может быть более гибким.
Пример использования лямбда-выражений
Предположим, вы создаете социальное сетевое приложение. Теперь вы хотите разработать функцию, позволяющую администраторам выполнять различные операции на пользователях, таких как поиск, распечатать, получить почту, и так далее. Предположим, что пользователи социальных сетейPerson
Класс означает:
public class Person {
public enum Sex {
MALE, FEMALE
}
private String name;
private LocalDate birthday;
private Sex gender;
private String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
Предположим, что все пользователи приложения социальной сети хранятся вList<Person>
в экземпляре.
Сначала мы используем простой метод для достижения этого варианта использования, затем используем локальные классы, анонимные внутренние классы для достижения и, наконец, делаем эффективную и краткую реализацию с помощью лямбда-выражений.
Способ 1. Создайте метод, который сопоставляет пользователей на основе функционального запроса.
Проще всего создать несколько функций, каждая из которых ищет заданные характеристики пользователя, напримерsearchByAge()
Этот метод следующий метод печатает всех пользователей, возраст которых больше определенного значения:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
Этот метод потенциально проблематичен, и программа завершится ошибкой, если будут внесены некоторые изменения (например, новые типы данных). Предположим, приложение обновляется и изменяетсяPerson
классы, например использование года рождения вместо возраста, также возможно, что алгоритм поиска возраста отличается. Таким образом, вам не придется писать много API для учета этих изменений.
Способ 2. Создайте более общий метод поиска
По сравнению с этим методомprintPersonsOlderThan
Более общий; он предоставляет пользователям, которые могут печатать определенный возрастной диапазон:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
Что делать, если вы хотите напечатать определенный пол или пользователей, которые соответствуют как определенному полу, так и определенному возрастному диапазону? Что делать, если вы хотите изменить класс Person и добавить другие атрибуты, такие как любовный статус и географическое положение? Хотя этот методprintPersonsOlderThan
Этот подход является более общим, но создание специальных функций для каждого запроса может сделать программу менее надежной. Вы можете использовать интерфейс для переноса конкретного поиска на конкретный класс, который необходимо найти (идея интерфейсно-ориентированного программирования — простой фабричный паттерн).
Способ 3: Установите конкретное состояние поиска в локальном классе
Следующий метод может распечатать всю информацию о пользователе, соответствующую критериям поиска.
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
Этот метод вызываетсяtester.test
метод обнаружения каждогоroster
Удовлетворяют ли элементы в списке критериям поиска. еслиtester.test
Возвратите true, затем распечатайте подходящееPerson
пример.
путем реализацииCheckPerson
Интерфейс реализует поиск.
interface CheckPerson {
boolean test(Person p);
}
Следующий класс реализуетCheckPerson
интерфейсtest
метод. еслиPerson
Свойство принадлежит мужчине и возраст от 18 до 25 вернет true
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
Если вы хотите использовать этот класс, вам просто нужно создать экземпляр и передать его в качестве параметра вprintPersons
метод.
printPersons(roster, new CheckPersonEligibleForSelectiveService());
Хотя этот подход менее хрупок - когдаPerson
Вам не нужно заново создавать дополнительные методы при внесении изменений, но вам все равно нужно добавить некоторый код: создать локальный класс для каждого критерия поиска, реализующего интерфейс.CheckPersonEligibleForSelectiveService
Класс реализует интерфейс, вы можете использовать анонимный внутренний класс вместо локального класса, объявив новый внутренний класс для удовлетворения различных запросов.
Способ 4: указать критерии поиска в анонимном внутреннем классе
следующееprintPersons
Второй параметр вызова функции является анонимным классом внутреннего класса, который фильтрует пользователи, гендерки которого - это мужчина, а возраст которого составляет от 18 до 25:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
Такой подход сокращает объем кода, поскольку вам не нужно создавать новый класс для каждого критерия поиска. Однако, учитываяCheckPerson
Интерфейс имеет только одну функцию, а синтаксис анонимного внутреннего класса немного неуклюж. В этом случае рассмотрите возможность замены анонимных внутренних классов лямбда-выражениями, как описано ниже.
Способ 5: Реальный интерфейс поиска через лямбда-выражение
CheckPerson
интерфейс представляет собойфункциональный интерфейс. Интерфейс только с одним абстрактным методом в интерфейсе является функциональным интерфейсом (функциональный интерфейс может также включать один или несколько методов по умолчанию или статических методов). Поскольку функциональный интерфейс содержит только один абстрактный метод, вы можете опустить имя метода при его реализации. Поэтому вы можете использоватьлямбда-выражениеВместо выражения анонимного внутреннего класса вызовите его так:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
Синтаксис лямбда-выражений будет подробно описан позже. вы также можете использоватьСтандартный функциональный интерфейсзаменятьCheckPerson
интерфейс, что еще больше сокращает объем кода.
Подход 6: использование стандартных функциональных интерфейсов и лямбда-выражений
CheckPerson
Интерфейс очень простой интерфейс:
interface CheckPerson {
boolean test(Person p);
}
Он имеет только один абстрактный метод, поэтому это функциональный интерфейс. Эта функция имеет параметр и возвращаемое значение. Слишком просто не обязательно определить его в вашем приложении. Следовательно, некоторые стандартные функциональные интерфейсы определены в JDK, что может бытьjava.util.function
нашел в упаковке. Например, вы можете использоватьPredicate<T>
заменятьCheckPerson
. Этот интерфейс содержит толькоboolean test(T t)
метод.
interface Predicate<T> {
boolean test(T t);
}
Predicate<T>
Является универсальным интерфейсом, и универсальный тип должен указывать один или несколько параметров в угловых скобках (). В этот интерфейс включен только один параметр T. Когда вы объявляете или создаете экземпляр дженерика с параметром реального типа, вы получаете параметризованный тип. Например, параметризованный типPredicate<Person>
Как показано в следующем коде:
interface Predicate<Person> {
boolean test(Person t);
}
Параметризованный интерфейс содержит интерфейс, аналогичный интерфейсуCheckPerson.boolean test(Person p)
точно так же. Таким образом, вы можете использовать код нижеPredicate<T>
заменятьCheckPerson
:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
Затем функцию можно вызвать следующим образом:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
Это не единственный способ использования лямбда-выражений. Ниже рекомендуется использовать лямбда-выражения другими способами.
Способ 7: используйте лямбда-выражения во всем приложении
посмотрим какprintPersonsWithPredicate
Где еще можно использовать лямбда-выражения:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
Этот метод обнаруживаетroster
каждый изPerson
Удовлетворяет ли экземплярtester
стандарт. Если экземпляр Person удовлетворяетtester
установлен стандарт, тоPerson
Информация об экземпляре будет распечатана.
Вы можете указать другое действие для выполнения печатиtester
критериев поиска, определенных вPerson
пример. Вы можете указать, что действие является лямбда-выражением. Предположим, вам нужна функция иprintPerson
То же самое лямбда-выражение (один параметр, возврат void), нужно реализовать функциональный интерфейс. В этом случае вам нужен функциональный интерфейс, содержащий единственный параметр типа Person и возвращающий значение void.Consumer<T>
Изменить пакет интерфейсаvoid accept(T t)
функцию, отвечающую вышеуказанным требованиям. Следующая функция используетConsumer<Person>
перечислитьaccept()
тем самым заменивp.printPerson()
вызов.
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
Затем вы можете позвонить такprocessPersons
функция:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
Что, если вы хотите сделать больше с информацией о пользователе, чем просто распечатать ее? Предположим, вы хотите проверить личную информацию участника или получить информацию об их контактах? В этом случае вам нужен функциональный интерфейс с абстрактной функцией, возвращающей значение.Function<T,R>
Интерфейс содержитR apply(T t)
метод, который имеет один параметр и одно возвращаемое значение. Следующий метод получает данные, соответствующие параметру, а затем выполняет соответствующую обработку в соответствии с блоком кода лямбда-выражения:
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
Функция ниже начинается сroster
Получите адреса электронной почты пользователей, соответствующих критериям поиска, и распечатайте адреса.
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
Метод 8: используйте дженерики, чтобы сделать его более универсальным
переработкаprocessPersonsWithFunction
функции, следующие функции могут принимать коллекции, содержащие данные любого типа:
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
Вышеупомянутая функция может быть вызвана следующим образом для печати почтовых ящиков пользователей, соответствующих критериям поиска:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
Вызов называется следующим действием:
- Получить объект из коллекции, в данном примере это обертка
Person
примерroster
собирать.roster
Является типом списка, но также и итерируемым типом. - фильтровать совпадения
Predicate
тип данныхtester
Объект. В этом примере объект Predicate представляет собой лямбда-выражение, задающее критерии поиска. - использовать
Function
Преобразователь типа сопоставляет каждый объект, который соответствует критериям фильтра. В этом примере объект Function возвращает адрес электронной почты пользователя. - Выполняет вызов для каждого объекта, сопоставленного с
Consumer
Действия, определенные в блоке объекта. В этом примере объект Consumer представляет собой лямбда-выражение, которое печатает адрес электронной почты, возвращаемый объектом Function.
Вы можете заменить вышеуказанную операцию агрегатной операцией.
Способ 9: операция слияния с использованием лямбда-выражения в качестве параметра
В следующем примере используется операция агрегирования, печатается адрес электронной почты пользователя, который соответствует критериям поиска:
roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
Следующая таблица сопоставленаprocessElements
Операции выполнения функций и соответствующие им агрегатные операции
процессЭлементыДействие | Агрегатная операция |
---|---|
получить источник объекта | Stream stream() |
фильтровать совпаденияПредикатный объектПримеры (лямбда-выражение) | Stream filter(Predicate<? super T> predicate) |
использоватьФункциональный объектСопоставляет объекты, соответствующие критериям фильтра, со значением | Stream map(Function<? super T,? extends R> mapper) |
воплощать в жизньПотребительский объект(лямбда-выражение) задать действие | void forEach(Consumer<? super T> action) |
filter
,map
иforEach
является операцией агрегации. Совокупная операция отstream
обрабатывать отдельные элементы из коллекции, а не непосредственно из коллекции (поэтому первая вызываемая функцияstream()
). steam предназначен для сериализации каждого элемента. В отличие от коллекции, это не структура данных, в которой хранятся данные. И наоборот, потоки загружают значения из источников, таких как коллекции, черезpipeline
Загрузить данные в поток.pipeline
Это операция сериализации потока. В этом примере этоfilter- map-forEach
. Кроме того, агрегатные операции часто могут получать лямбда-выражение в качестве параметра, поэтому вы можете настроить нужное действие.
Использование лямбда-выражений в программах с графическим интерфейсом
Для обработки событий в приложении с графическим пользовательским интерфейсом (GUI), таких как события ввода с клавиатуры, события движения мыши и события прокрутки, вы обычно реализуете определенный интерфейс для создания обработчика событий. Обычно интерфейс обработки времени является функциональным интерфейсом, и у них обычно есть только одна функция.
Время, соответствующее предыдущей реализации с использованием анонимных внутренних классов:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
Вместо этого вы можете использовать следующий код:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Синтаксис лямбда-выражения
Лямбда-выражение состоит из следующей структуры:
-
()
Заключите параметры и разделите их запятыми, если параметров несколько.CheckPerson.test
Функция имеет один параметр p, который представляет экземпляр Person.Уведомление:Вы можете опустить типы параметров в лямбда-выражениях. Также скобки можно не указывать, если параметр всего один. Например, следующее лямбда-выражение также допустимо:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
- Символ стрелки: ->
- Тело: состоит из выражения или блока объявлений. В примере используется такое выражение:
p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Если вы укажете выражение, среда выполнения Java оценит выражение и вернет результат. Между тем, вы можете использовать оператор return:
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
не является выражением, возвращаемым в лямбда-выражении, вы должны использовать{}
Вложите блоки кода. Однако, когда возвращаетсяvoid
Нам не нужны типы скобок. Например, следующее является действительным выражением лямбда:
email -> System.out.println(email)
Лямбда-выражение немного похоже на объявленную функцию, и вы можете думать о лямбда-выражении как об анонимной функции (функции без имени).
Ниже приведен пример лямбда-выражения с несколькими параметрами:
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " +
myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " +
myApp.operateBinary(20, 10, subtraction));
}
}
методoperateBinary
Выполните математические операции над двумя числами. Сама операция правильнаяIntegerMath
инстанцирование класса. В примере две операции определяются лямбда-выражениями: сложение и вычитание. Пример вывода выглядит следующим образом:
40 + 2 = 42
20 - 10 = 10
Получить локальную переменную в закрытии
Подобно локальным и анонимным классам, лямбда-выражения могут обращаться к локальным переменным, они имеют доступ к локальным переменным. Лямбда-выражение также относится к текущей области видимости, то есть оноНе наследовать именованные имена из родительской области или вводить новый уровень области.. Область действия лямбда-выражения — это область, в которой оно было объявлено. Следующий пример иллюстрирует это:
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
Consumer<Integer> myConsumer = (y) ->
{
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
Будет выведена следующая информация:
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0
Если в лямбда-выражении, как показано нижеmyConsumer
Используйте x вместо параметра y, тогда компиляция завершится ошибкой.
Consumer<Integer> myConsumer = (x) -> {
}
Компиляция покажет, что "переменная x уже определена в методе methodInFirstLevel(int)", потому что лямбда-выражение не вводит новую область (область, в которой находится лямбда-выражение, уже имеет x определенную). Следовательно, возможен прямой доступ к переменным-членам замыкания, где находится лямбда-выражение, функция и локальные переменные в замыкании. Например, лямбда-выражение может напрямую обращаться к параметру x метода methodInFirstLevel. Доступ к областям уровня класса можно получить с помощью ключевого слова this. В этом примере this.x является значением переменной-члена FirstLevel.x.
Однако, подобно локальным и анонимным классам, значения лямбда-выражения могут обращаться к локальным переменным и параметрам, оформленным как final или эффективно final. Например, предположим, что вmethodInFirstLevel
Добавьте в объявление следующее определение:
Эффективно окончательный: значение переменной или параметра не изменяется после инициализации, поэтому переменная или параметр являются фактически окончательными.
void methodInFirstLevel(int x) {
x = 99;
}
так какx =99
заявление делаетmethodInFirstLevel
Параметр x больше не относится к эффективному типу final. В результате компилятор java сообщит об ошибке типа «локальные переменные, на которые ссылается лямбда-выражение, должны быть окончательными или фактически окончательными».
Тип цели
Как java определяет тип данных лямбда-выражения во время выполнения? Посмотрите еще раз на лямбда-выражение, которое выбирает пол как мужской и возраст от 18 до 25 лет:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
Это лямбда-выражение передается в качестве параметров следующим двум функциям:
- public static void printPersons(List roster, CheckPerson tester)
- public void printPersonsWithPredicate(List roster, Predicate tester)
метод вызова при запущенной JavaprintPersons
, он ожидаетCheckPerson
тип данных, поэтому лямбда-выражения относятся к этому типу. метод вызова при запущенной JavaprintPersonsWithPredicate
, он ожидаетPredicate<Person>
тип данных, поэтому лямбда-выражение является таким типом. Тип данных, ожидаемый этими методами, называется целевым типом. Чтобы определить тип лямбда-выражения, компилятор Java определяет его целевой тип в контексте лямбда-выражения. Лямбда-выражение может быть выполнено только в том случае, если целевой тип может быть выведен компилятором java.
тип цели и параметры функции
Для параметров функций компилятор Java может определить целевой тип с помощью двух других функций языка: разрешения перегрузки и вывода параметра типа. Посмотрите на следующие два функциональных интерфейса ( java.lang.Runnable и java.util.concurrent.Callable ):
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
методRunnable.run
не возвращает значения, ноCallable<V>.call
Имеет возвращаемое значение. Предположим, вы перегружаете метод, как показано ниже.invoke
:
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
Затем выполните следующую программу, какой метод будет называться как?
String s = invoke(() -> "done");
методinvoke(Callable<T>)
будет вызван, потому что этот метод возвращает значение; методinvoke(Runnable)
Нет возвращаемого значения. В этом случае лямбда-выражение() -> "done"
ТипCallable<T>
.
Наконец
Спасибо, что прочитали. Если вам интересно, вы можете подписаться на общедоступную учетную запись WeChat, чтобы получать последние push-статьи.