Использование лексической области видимости и замыканий
У многих разработчиков возникает это недоразумение, полагая, что использование лямбда-выражений приведет к избыточности кода и снизит его качество. Напротив, каким бы сложным ни был код, мы не будем идти на компромиссы в отношении качества кода ради простоты, как увидим ниже.
Мы смогли повторно использовать лямбда-выражение в предыдущем примере, однако, если мы сопоставим другую букву, проблема избыточности кода быстро вернется; Давайте сначала проанализируем эту проблему подробнее, а затем воспользуемся лексической областью видимости и замыканиями для ее решения.
Избыточность, вызванная лямбда-выражениями
Давайте отфильтруем от друзей буквы, начинающиеся с N или B. Продолжая приведенный выше пример, код, который мы пишем, может выглядеть следующим образом:
Скопируйте код кода следующим образом:
Final Predicate<String> startWithN = name -> name.startsWith("N");
Final Predicate<String> startWithB = name -> name.startsWith("B");
окончательный длинный счетчикFriendsStartN =
друзья.поток()
.filter(startsWithN).count();
окончательный длинный счетчикFriendsStartB =
друзья.поток()
.filter(startsWithB).count();
Первый предикат определяет, начинается ли имя с буквы N, а второй — начинается ли имя с буквы B. Мы передаем эти два экземпляра двум вызовам метода фильтра соответственно. Это кажется разумным, но два предиката избыточны, это просто разные буквы в проверке. Давайте посмотрим, как можно избежать этой избыточности.
Используйте лексическую область видимости, чтобы избежать избыточности.
В первом решении мы можем извлечь буквы как параметры функции и передать эту функцию методу фильтра. Это хороший метод, но фильтр принимается не всеми функциями. Он принимает только функции только с одним параметром. Этот параметр соответствует элементу в коллекции и возвращает логическое значение. Он надеется, что переданное значение является предикатом.
Мы надеемся, что есть место, где эту букву можно будет кэшировать до тех пор, пока не будет передан параметр (в данном случае параметр name). Давайте создадим новую функцию, подобную этой.
Скопируйте код кода следующим образом:
public static Predicate<String> checkIfStartsWith(последняя буква строки) {
вернуть имя -> name.startsWith(буква);
}
Мы определили статическую функцию checkIfStartsWith, которая получает параметр String и возвращает объект Predicate, который можно передать методу фильтра для дальнейшего использования. В отличие от функций высшего порядка, которые мы видели ранее и которые принимают функции в качестве параметров, этот метод возвращает функцию. Но это также функция более высокого порядка, о которой мы уже упоминали в разделе «Эволюция, а не изменение» на стр. 12.
Объект Predicate, возвращаемый методом checkIfStartsWith, несколько отличается от других лямбда-выражений. В возвращаемом операторе name -> name.startsWith(letter) мы точно знаем, что такое имя, это параметр, передаваемый в лямбда-выражение. Но что такое переменная буква? Он находится за пределами домена анонимной функции. Java находит домен, в котором определено лямбда-выражение, и обнаруживает букву переменной. Это называется лексической областью действия. Лексическая область видимости — очень полезная вещь, она позволяет нам кэшировать переменную в одной области видимости для последующего использования в другом контексте. Поскольку это лямбда-выражение использует в своей области действия переменные, такая ситуация также называется замыканием. Что касается ограничений доступа лексической области, можете ли вы прочитать ограничения лексической области на странице 31?
Существуют ли какие-либо ограничения по лексическому объему?
В лямбда-выражении мы можем получить доступ только к конечным типам в их области действия или к локальным переменным конечного типа.
Лямбда-выражение может быть вызвано немедленно, с задержкой или из другого потока. Чтобы избежать расовых конфликтов, локальные переменные в домене, к которому мы обращаемся, не могут быть изменены после инициализации. Любая операция модификации вызовет исключение компиляции.
Пометка его как окончательного решает эту проблему, но Java не заставляет нас помечать его таким образом. Фактически, Java смотрит на две вещи. Во-первых, переменная, к которой осуществляется доступ, должна быть инициализирована в методе, в котором она определена, и до того, как будет определено лямбда-выражение. Во-вторых, значения этих переменных не могут быть изменены — то есть они фактически имеют тип Final, хотя и не помечены как таковые.
Лямбда-выражения без сохранения состояния являются константами времени выполнения, тогда как те, которые используют локальные переменные, требуют дополнительных вычислительных затрат.
При вызове метода фильтра мы можем использовать лямбда-выражение, возвращаемое методом checkIfStartsWith, например:
Скопируйте код кода следующим образом:
окончательный длинный счетчикFriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
окончательный длинный счетчикFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
Прежде чем вызвать метод фильтра, мы сначала вызвали метод checkIfStartsWith() и передали нужные буквы. Этот вызов быстро возвращает лямбда-выражение, которое затем передается методу фильтра.
Создав функцию высшего порядка (в данном случае checkIfStartsWith) и используя лексическую область видимости, мы успешно устранили избыточность из кода. Нам больше не нужно повторно определять, начинается ли имя с определенной буквы.
Рефакторинг, сокращение объема
В предыдущем примере мы использовали статический метод, но мы не хотим использовать статический метод для кэширования переменных, иначе это испортит наш код. Лучше всего сузить область действия этой функции до места, где она используется. Для этого мы можем использовать интерфейс Function.
Скопируйте код кода следующим образом:
окончательная Function<String, Predicate<String>> BeginsWithLetter = (Строковая буква) -> {
Predicate<String> checkStarts = (имя строки) -> name.startsWith(буква);
вернуть checkStars };
Это лямбда-выражение заменяет исходный статический метод. Его можно поместить в функцию и определить до того, как оно понадобится. Переменная startWithLetter относится к функции, входным параметром которой является строка, а выходным параметром — предикат.
По сравнению со статическим методом эта версия намного проще, но мы можем продолжить ее рефакторинг, чтобы сделать ее более краткой. С практической точки зрения эта функция аналогична предыдущему статическому методу: они одновременно получают строку и возвращают предикат; Вместо явного объявления предиката мы полностью заменяем его лямбда-выражением.
Скопируйте код кода следующим образом:
окончательная функция<String, Predicate<String>> startWithLetter = (строковая буква) -> (строковое имя) -> name.startsWith(буква);
Мы избавились от беспорядка, но мы также можем удалить объявление типа, чтобы сделать его более кратким, и компилятор Java выполнит вывод типа на основе контекста. Давайте посмотрим на улучшенную версию.
Скопируйте код кода следующим образом:
окончательная функция<String, Predicate<String>> BeginsWithLetter =
буква -> имя -> name.startsWith(буква);
Требуется некоторое усилие, чтобы адаптироваться к этому краткому синтаксису. Если это вас ослепляет, сначала посмотрите в другом месте. Мы завершили рефакторинг кода и теперь можем использовать его для замены исходного метода checkIfStartsWith(), например:
Скопируйте код кода следующим образом:
окончательный длинный счетчикFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
окончательный длинный счетчикFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
В этом разделе мы используем функции высшего порядка. Мы увидели, как создавать функции внутри функций, если мы передаем функцию другой функции, и как возвращать функцию из функции. Все эти примеры демонстрируют простоту и возможность повторного использования лямбда-выражений.
В этом разделе мы полностью использовали функции Function и Predicate, но давайте посмотрим на разницу между ними. Предикат принимает параметр типа T и возвращает логическое значение, представляющее истинное или ложное значение соответствующего условия оценки. Когда нам нужно вынести условное суждение, мы можем использовать Predicateg для его завершения. Такие методы, как filter, в которых элементы фильтра получают Predicate в качестве параметра. Funciton представляет функцию, входными параметрами которой являются переменные типа T, и возвращает результат типа R. Он более общий, чем Predicate, который может возвращать только логическое значение. Пока ввод преобразуется в вывод, мы можем использовать функцию, поэтому для карты разумно использовать функцию в качестве параметра.
Как видите, выбирать элементы из коллекции очень просто. Ниже мы покажем, как выбрать только один элемент из коллекции.