Почему @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")));