JEP draft: fluent postfix notation for statically scoped interface methods
постфиксная fluent-нотация для статически разрешаемых методов интерфейсов
| Ответственный | John Rose |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | specification / language |
| Создан | 2017/11/18 06:34 |
| Обновлён | 2021/02/06 00:25 |
| Задача | 8191530 |
постфиксная fluent-нотация для статически разрешаемых методов интерфейсов
Методы интерфейсов по умолчанию (default methods) — важный способ выражать переопределяемые алгоритмы, общие для всех реализаций интерфейса. Хорошо подобранный набор методов по умолчанию позволяет интерфейсу (например, Stream) предоставлять «маленький DSL»: методы вызываются на объекте по одному, слева направо, и результат каждого метода становится получателем следующего вызова. Такой стиль называют «fluent» (текучим).
Иногда метод интерфейса нельзя выразить как метод по умолчанию — не потому, что fluent-синтаксис был бы неверен, а из соображений безопасности и целостности программы. Коротко говоря, некоторые методы невозможно достаточно надёжно реализовать в интерфейсах, поэтому их нужно вызывать в режиме, который не допускает переопределения.
Основной пример — вызовы, создающие неизменяемую копию. Если разрешить определять метод copyAsUnmodifiable (он же «замораживающий» метод) как обычный метод по умолчанию, то некорректная реализация сможет вернуть изменяемый результат. Такие ошибки случаются достаточно часто, чтобы о них беспокоиться: иногда случайно, а иногда как часть уязвимости.
Язык должен допускать некий маркер у статических методов интерфейсов, который позволит компилятору принимать вызов статического метода так, как если бы это был нестатический метод, где первый аргумент метода стоит в позиции получателя.
Например:
interface List<T> {
__Fluent static <T> List<T> frozen(List<T> self) {
if (self instanceof ImmutableList) return self;
return new ImmutableList(self);
}
}
// and then:
<T> void processSecurely(List<T> input) {
List<T> safeInput = input.asFrozen();
...do something confident that safeInput won't change...
}
// that was sugar for:
<T> void processSecurely2(List<T> input) {
List<T> safeInput = List.asFrozen(input);
...do something confident that safeInput won't change...
}
Fluent-статический вызов ls.asFrozen() на самом деле был бы статическим вызовом, неотличимым от List.asFrozen(ls). Fluent-синтаксис был бы разрешён, только если у типа левого выражения ls ещё нет применимого нестатического метода с тем же именем и совместимыми параметрами.
// example of conflict between virtual and static:
class BadList implements List<Object> {
List<Object> asFrozen() { return new WorseList(this); }
}
void doSomething(BadList input) {
input.asFrozen(); // broken or nefarious call, but only on narrow type
((List<Object>)input).asFrozen(); // calls good fluent static
}
Результат был бы примерно таким же, как при разрешении неоднозначного вызова метода foo(), когда существует и метод foo() у this, и статический импорт foo(). Виртуальный метод имеет приоритет над статическим.
Разрешить в интерфейсе метод «final default» — неверный подход, поскольку это накладывает тяжёлое и неконтролируемое ограничение на классы, которые будут реализовывать интерфейс.
Небольшое преимущество fluent-статических методов перед обычными состоит в том, что вывод обобщённых типов по аргументам статического метода работает лучше, чем по получателю виртуального обобщённого метода.
Помимо заморозки, завершающая операция выражения с построителем (builder) может потребоваться в виде fluent-статического метода вместо виртуального, если существует некий инвариант (например, стабильность), который сам интерфейс построителя хочет обеспечить и не доверяет его соблюдение подтипам интерфейса построителя. Разумеется, это зависит от того, может ли некорректная реализация «просочиться» в выражение с построителем и затем сломать завершающий вызов. В большинстве случаев, я полагаю, это не проблема.