JEP 302: Lambda Leftovers
Оставшиеся доработки Lambda
| Ответственный | Maurizio Cimadamore |
| Тип | Feature |
| Область | SE |
| Статус | Candidate |
| Компонент | tools / javac |
| Обсуждение | platform dash jep dash discuss at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | M |
| Создан | 2016/11/25 16:24 |
| Обновлён | 2017/04/11 16:16 |
| Задача | 8170361 |
Аннотация
Сделать лямбда-выражения и ссылки на методы удобнее: улучшить разрешение неоднозначностей функциональных выражений в контекстах вызова методов, завершить возвращение символу подчёркивания роли обозначения неиспользуемых параметров лямбда-выражений и разрешить параметрам лямбда-выражений затенять переменные из объемлющих областей видимости.
Описание
Обработка символа подчёркивания
Во многих языках символ подчёркивания (_) принято использовать для обозначения безымянного параметра лямбда-выражения (а также параметров методов и исключений):
BiFunction<Integer, String, String> biss = (i, _) -> String.valueOf(i);
Так можно строже проверять неиспользуемые аргументы на этапе компиляции, а также помечать неиспользуемыми сразу несколько аргументов. Однако в Java 8 подчёркивание было допустимым идентификатором, поэтому из соображений совместимости нам пришлось идти к тому, чтобы подчёркивание могло играть эту роль в Java, окольным путём. На этапе 1 в Java 8 подчёркивание было запрещено как имя формального параметра лямбда-выражения (это не нарушало совместимость, поскольку раньше лямбда-выражений не было), а на использование подчёркивания как идентификатора в других местах выдавалось предупреждение. Этап 2 наступил в Java 9, когда это предупреждение стало ошибкой. Теперь мы можем завершить запланированное возвращение подчёркиванию роли обозначения неиспользуемого формального параметра лямбда-выражения, метода или блока catch.
Затенение параметров лямбда-выражений
Параметрам лямбда-выражений запрещено затенять переменные из объемлющих областей видимости. (Иначе говоря, лямбда-выражение ведёт себя как оператор for — см. JLS.) Это часто создаёт проблемы, как в следующем (очень распространённом) случае:
Map<String, Integer> msi = ...
...
String key = computeSomeKey();
msi.computeIfAbsent(key, key -> key.length()) //error
Здесь попытка повторно использовать имя key для параметра лямбда-выражения в вызове computeIfAbsent завершается ошибкой, поскольку переменная с тем же именем уже определена в объемлющем контексте.
Было бы желательно снять это ограничение и разрешить параметрам лямбда-выражений (и локальным переменным, объявленным внутри лямбда-выражения) затенять переменные, определённые в объемлющих областях видимости. (Один из возможных доводов против — читаемость: если параметрам лямбда-выражений разрешить затенение, то в приведённом выше примере идентификатор 'key' означает две разные вещи в двух местах, где он используется, и, по-видимому, никакой синтаксической границы, разделяющей эти два использования, нет.)
Необязательно: улучшенное разрешение неоднозначностей для функциональных выражений
В Java SE 8 разрешение перегрузки было полностью переработано, чтобы обеспечить более глубокое взаимодействие с выводом типов. В Java SE 8 проверке применимости подлежат не все выражения-аргументы в вызове метода, а только те, что существенны для применимости (pertinent to applicability). Лямбда-выражения и ссылки на методы могут относиться к обеим категориям: явные лямбда-выражения (explicit lambdas) и точные ссылки на методы (exact method refs) существенны для применимости, а неявные лямбда-выражения (implicit lambdas) и неточные ссылки на методы (inexact method refs) — нет.
Если выражение не существенно для применимости, точность проверки применимости сильно снижается: такие выражения не проходят полную проверку (т. е. проверку совместимости с целевым типом), а проходят гораздо более слабую проверку, называемую потенциальной совместимостью (potential compatibility), которая проверяет только следующее:
- арность лямбда-выражения или ссылки на метод совпадает с арностью целевого функционального типа;
- лямбда-выражение или ссылка на метод, совместимые со значением (соответственно с void), используются с функциональным типом, совместимым со значением (соответственно с void).
В частности, компилятору не разрешено выполнять:
- атрибуцию тела лямбда-выражения;
- разрешение перегрузки для ссылки на метод.
В результате способность компилятора отсеивать неприменимые кандидаты сильно ограничена. Это был сознательный компромисс, чтобы избежать хрупкости (т. е. ситуации, когда ошибки в теле лямбда-выражения влияют на решения при разрешении перегрузки и, следовательно, изменения в теле лямбда-выражения могут влиять на эти решения) и комбинаторного взрыва стоимости проверки типов. Тем не менее в рамках этих ограничений возможны небольшие улучшения, позволяющие точнее проверять применимость. Это позволило бы нам устранить некоторые случайные ошибки неоднозначности, которые довольно регулярно возникают в реальном коде на Java. Рассмотрим следующий пример:
m(Predicate<String> ps) { ... }
m(Function<String, String> fss) { ... }
m(s -> false) //ambiguous
Пользователю очевидно, какой перегруженный метод здесь должен быть выбран: лямбда-выражение возвращает false, поэтому Function<String, String> явно несовместим. Но для компилятора лямбда-выражение не существенно для применимости, оба метода применимы, ни один не является более специфичным, и в результате возникает ошибка неоднозначности.
Похожая проблема возникает с неточными ссылками на методы:
class Foo {
static boolean g(String s) { return false }
static boolean g(Integer i) { return false }
}
m(Foo::g) //ambiguous
И снова мы сталкиваемся с проблемами: хотя один из методов (g(Integer)) явно несовместим с обоими целевыми типами (которые ожидают аргумент, совместимый с String), компилятору не разрешено анализировать ссылку на метод, и снова выдаётся ошибка неоднозначности.
Ключевое наблюдение, которое можно использовать для устранения этих «случайных неоднозначностей», состоит в том, что во всех этих случаях все кандидаты перегрузки накладывают общее ограничение на тип в функциональном дескрипторе: в примерах выше, какой бы целевой метод ни был выбран, итоговый функциональный дескриптор всегда будет иметь тип аргумента String. Поэтому компилятор мог бы без риска считать, что неявный параметр лямбда-выражения в первом примере выше имеет тип String, — это позволяет компилятору проверить типы в теле лямбда-выражения, а затем по типу возвращаемого значения лямбда-выражения отбросить один из двух кандидатов, как и ожидается. Двойственная ситуация имеет место в примере со ссылкой на метод выше: здесь разрешение перегрузки для символа ссылки на метод можно выполнить до объемлющего разрешения перегрузки (поскольку неточная ссылка на метод в своём раунде разрешения перегрузки всегда будет использовать String в качестве фактического типа, независимо от целевого типа).
Для ссылок на методы возможно ещё одно улучшение. Рассмотрим следующий пример, который сейчас не компилируется:
m2(Predicate<String> ps) { ... }
m2(Function<Integer, String> fss) { ... }
class Baz {
static String f(Double d) { ... }
static String f(Integer i) { ... }
}
m2(Baz::f)
Здесь ссылка на метод также считается неточной, то есть мы не можем использовать тип возвращаемого значения f, чтобы отсеять кандидатов перегрузки. Ключевое наблюдение и здесь в том, что тип возвращаемого значения у обоих вариантов одинаков: String. Поэтому можно было бы уточнить проверку потенциальной применимости (potentially applicable), чтобы учитывать это, и, следовательно, отсеять целевой тип Predicate<String> на том основании, что он несовместим с результатом String (который вы получили бы от обоих методов). Обратите внимание, что в этом случае условие, рассмотренное выше, не выполняется: два варианта m2 подразумевают функциональные дескрипторы с разными типами аргументов.
Примечание: этот раздел необязателен, поскольку его влияние на реализацию компилятора ещё предстоит оценить.