Почему @FunctionalInterface не может применяться к абстрактному базовому классу SAM
Я только начинаю изучать Camel, и первое, что я вижу, это
context.addRoutes(new RouteBuilder() {
public void configure() {
from("file:data/inbox?noop=true").to("file:data/outbox");
}
});
который я (разумно IMHO) пытаюсь заменить на
context.addRoutes(()->from("file:data/inbox?noop=true").to("file:data/outbox"));
но это неверно.
Как я роюсь, я обнаружил, что lambdas применяются к функциональным интерфейсам (что подразумевается, если интерфейс соответствует), но что аннотация @FunctionalInterface может применяться только к интерфейсам (достаточно справедливо), и есть, насколько я может сказать, нет эквивалентной аннотации для абстрактных классов. RouteBuilder - это, конечно, абстрактный класс.
Почему lambdas ограничен интерфейсами?
В чем существенное отличие между интерфейсом и классом, который делает "функциональный класс" небезопасным/непредсказуемым/необоснованным?
Я мог бы понять, был ли какой-то классификатор, например абстрактный метод, должен был быть общедоступным, но я затрудняюсь объяснить, почему вышеизложенное необоснованно.
Ответы
Ответ 1
Это было одно из самых сложных и широко обсуждаемых решений в Группе экспертов JSR-335. С одной стороны, представляется вполне разумным, что абстрактный абстрактный метод может быть разумной целью преобразования для лямбда. И если ваша ментальная модель "лямбда - это просто компактные анонимные классы", тогда это было бы вполне разумной идеей.
Однако, если вы некоторое время натягиваете эту строку, вы понимаете, что она тянет с вами много сложности и ограничений - ради использования меньшинства.
Одна из худших вещей, которые тащит с собой, - это смысл имен внутри тела лямбда, а в качестве особого случая - значение this
. Внутри внутреннего класса существует ужасно сложное правило поиска ( "поиск гребня" ), поскольку имена внутри внутреннего класса могут относиться к членам супертипа или могут быть захвачены из лексической среды. (Например, многие ошибки и головоломки вращаются вокруг, используя this
, а не Outer.this
, во внутренних телах класса.) Если мы разрешаем преобразование лямбда в абстрактные классы SAM, у нас будет два неправильных выбора; загрязнять все лямбды со сложной сложностью определения внутреннего класса или разрешать преобразование в объекты абстрактного класса, но ограничивать доступ таким образом, чтобы тело лямбда не могло ссылаться на членов базового класса (что вызовет его собственную путаницу). получившееся правило получается очень чистым: помимо форматов лямбда-параметров имена (включая this
, который является просто именем) внутри тела лямбда означают то, что они означают сразу за пределами лямбда-тела.
Другая проблема, связанная с переходом lambdas во внутренние классы, связана с идентификацией объекта и сопутствующей потерей оптимизации VM. В выражении внутреннего класса (например, new Foo() { }
) гарантируется уникальная идентификация объекта. Не совершая так сильно идентификацию объекта для lambdas, мы освобождаем виртуальную машину, чтобы сделать много полезных оптимизаций, которые она в противном случае не могла бы сделать. (В результате лямбда-связь и захват уже быстрее, чем для анонимных классов, - и до сих пор существует глубокий трубопровод оптимизации, который мы еще не применили.)
Кроме того, если у вас есть абстрактный класс абстрактного метода и вы хотите использовать lambdas для их создания, там есть простой путь к его включению - определите метод factory, который использует функциональный интерфейс как аргумент. (Мы добавили метод factory для ThreadLocal
в Java 8, который делает это.)
Последний гвоздь в гробу для "lambdas - просто удобный синтаксис для объектов", взгляд на мир появился после того, как мы провели анализ существующих кодовых баз и их использование интерфейсов с одним абстрактным методом и абстрактных классов. Мы обнаружили, что только очень небольшой процент был основан на абстрактных классах. Было глупо обременять все лямбды сложностью и проблемами производительности подхода, который только выиграл бы менее 1% использования. Поэтому мы приняли "смелое" решение отказаться от этого варианта использования, чтобы воспользоваться преимуществами, которые это обеспечило для других 99 +%.
Ответ 2
Лямбда-выражения определяют функции не методы. Очевидно, что между ними существует техническая связь, но она отличается концептуальным видом и тем, как он работает на уровне исходного кода.
например. выражение лямбда не наследует членов от типа, который он в конечном итоге реализует. Таким образом, в вашем случае это не работает, даже если RouteBuilder
был функциональным интерфейсом, поскольку ваше выражение лямбда не наследует метод from
, который вы хотите вызвать. Точно так же значения this
и super
являются такими же, как и вне лямбда-выражения, и не относятся к экземпляру, который будет представлять функцию после этого (т.е. Экземпляр RouteBuilder
).
Тем не менее, было бы нецелесообразно расширять функцию для реализации классов abstract
, которые ведут себя как interface
, но это накладывает несколько ограничений, которые трудно проверить. Хотя легко убедиться, что класс имеет ровно один метод abstract
и доступный конструктор no-arg, класс также должен быть свободен от любого изменчивого состояния, а построение экземпляра этого класса также должно быть свободным от побочных эффектов, так что свобода JVM кэшировать и повторно использовать экземпляры lambda и делиться ими между разными сайтами создания не влияет на поведение программы.
Трудно проверить и, в большинстве случаев, ограничения не выполняются, поскольку это причина использования abstract class
, а не interface
в первую очередь. Это могло бы работать, если лямбда-выражения были определены как замена только для внутренних классов, и поэтому совместное использование и повторное использование экземпляров не было разрешено, но это не то, что лямбда-выражения, даже если они используются как простая замена внутреннего класса в во многих случаях, не задумываясь о функциональном программировании...
Ответ 3
В дополнение к другим замечательным ответам следует упомянуть, что если вам действительно нужно создавать такие объекты RouteBuilder
очень часто, вы можете создать вспомогательный метод, подобный этому:
public static RouteBuilder fromConfigurator(Consumer<RouteBuilder> configurator) {
return new RouteBuilder() {
public void configure() {
configurator.accept(this);
}
}
}
И используйте его следующим образом:
context.addRoutes(fromConfigurator(
rb->rb.from("file:data/inbox?noop=true").to("file:data/outbox")));