openjdk.ruOpenJDK на русском

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-статического метода вместо виртуального, если существует некий инвариант (например, стабильность), который сам интерфейс построителя хочет обеспечить и не доверяет его соблюдение подтипам интерфейса построителя. Разумеется, это зависит от того, может ли некорректная реализация «просочиться» в выражение с построителем и затем сломать завершающий вызов. В большинстве случаев, я полагаю, это не проблема.