Почему мы не можем использовать методы по умолчанию в лямбда-выражениях?

Я читал этот учебник по Java 8, где автор показал код:

interface Formula {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

А потом сказал

Методы по умолчанию не могут быть доступны из лямбда-выражений. Следующий код не компилируется:

Formula formula = (a) -> sqrt( a * 100);

Но он не объяснил, почему это невозможно. Я запустил код, и он выдал ошибку,

несовместимые типы: Формула не является функциональным интерфейсом`

Так почему же это невозможно или в чем смысл ошибки? Интерфейс удовлетворяет требованию функционального интерфейса, имеющего один абстрактный метод.

5 ответов

Решение

Это более или менее вопрос масштаба. От JLS

В отличие от кода, появляющегося в анонимных объявлениях классов, значение имен и this а также super ключевые слова, появляющиеся в лямбда-теле, наряду с доступностью ссылочных объявлений, такие же, как в окружающем контексте (за исключением того, что лямбда-параметры вводят новые имена).

В вашем попытанном примере

Formula formula = (a) -> sqrt( a * 100);

область не содержит декларации для имени sqrt,

На это также намекают в JLS

Практически говоря, для лямбда-выражения является необычным необходимость говорить о себе (либо вызывать себя рекурсивно, либо вызывать его другие методы), в то время как более распространено желание использовать имена для ссылки на вещи во включающем классе, которые могли бы в противном случае быть в тени (this, toString()). Если необходимо, чтобы лямбда-выражение ссылалось на себя (как будто через this ), вместо него должна использоваться ссылка на метод или анонимный внутренний класс.

Я думаю, что это могло быть реализовано. Они решили не допустить этого.

Лямбда-выражения работают совершенно иначе, чем анонимные классы в этом this представляет то же самое, что и в области видимости выражения.

Например, это компилирует

class Main {

    public static void main(String[] args) {
        new Main().foo();
    }

    void foo() {
        System.out.println(this);
        Runnable r = () -> {
            System.out.println(this);
        };
        r.run();
    }
}

и это печатает что-то вроде

Main@f6f4d33
Main@f6f4d33

Другими словами this это Main, а не объект, созданный лямбда-выражением.

Таким образом, вы не можете использовать sqrt в вашем лямбда-выражении, потому что тип this ссылка не Formulaили подтип, и он не имеет sqrt метод.

Formula это функциональный интерфейс, хотя и код

Formula f = a -> a;

компилирует и запускает для меня без проблем.

Хотя вы не можете использовать лямбда-выражения для этого, вы можете сделать это с помощью анонимного класса, например так:

Formula f = new Formula() { 
    @Override 
    public double calculate(int a) { 
        return sqrt(a * 100); 
    }
};

Это не совсем так. Методы по умолчанию могут использоваться в лямбда-выражениях.

interface Value {
    int get();

    default int getDouble() {
        return get() * 2;
    }
}

public static void main(String[] args) {
    List<Value> list = Arrays.asList(
            () -> 1,
            () -> 2
        );
    int maxDoubled = list.stream()
        .mapToInt(val -> val.getDouble())
        .max()
        .orElse(0);
    System.out.println(maxDoubled);
}

печать 4 как ожидается и использует метод по умолчанию внутри лямбда-выражения (.mapToInt(val -> val.getDouble()))

Что автор вашей статьи пытается сделать здесь

Formula formula = (a) -> sqrt( a * 100);

это определить Formula, который работает как функциональный интерфейс, напрямую через лямбда-выражение.

Это работает нормально, в приведенном выше примере кода, Value value = () -> 5 или с Formula как интерфейс например

Formula formula = (a) -> 2 * a * a + 1;

Но

Formula formula = (a) -> sqrt( a * 100);

не удается, потому что он пытается получить доступ к (this.)sqrt метод, но это не может. Лямбды в соответствии со спецификацией наследуют свою сферу от окружающей среды, а это означает, что this Внутри лямбда относится к тому же предмету, что и непосредственно снаружи. И нет sqrt метод снаружи.

Мое личное объяснение этому: внутри лямбда-выражения не совсем понятно, в какой конкретный функциональный интерфейс лямбда будет "преобразована". сравнить

interface NotRunnable {
    void notRun();
}

private final Runnable r = () -> {
    System.out.println("Hello");
};

private final NotRunnable r2 = r::run;

Одно и то же лямбда-выражение может быть "приведено" к нескольким типам. Я думаю об этом, как будто лямбда не имеет типа. Это специальная функция без типа, которая может использоваться для любого интерфейса с правильными параметрами. Но это ограничение означает, что вы не можете использовать методы будущего типа, потому что вы не можете знать это.

Это немного добавляет к обсуждению, но я все равно нашел его интересным.

Другой способ увидеть проблему - подумать об этом с точки зрения самообращающейся лямбды.

Например:

Formula formula = (a) -> formula.sqrt(a * 100);

Казалось бы, это должно иметь смысл, так как к тому времени, когда лямбда начинает выполняться, formula ссылка должна быть уже инициализирована (т.е. нет способа сделать formula.apply() до тех пор formula был должным образом инициализирован, в случае чего, из тела лямбды, тела apply, должно быть возможно ссылаться на одну и ту же переменную).

Однако это тоже не работает. Интересно, что раньше это было возможно в начале. Вы можете видеть, что Морис Нафталин зафиксировал это на своем веб-сайте Lambda FAQ. Но по какой-то причине поддержка этой функции была в конечном итоге удалена.

Некоторые из предложений, приведенных в других ответах на этот вопрос, уже упоминались в самой дискуссии в лямбда-рассылке.

К методам по умолчанию можно получить доступ только с помощью ссылок на объекты. Если вы хотите получить доступ к методу по умолчанию, у вас будет ссылка на объект из функционального интерфейса, в теле метода лямбда-выражения у вас его не будет, поэтому вы не сможете получить к нему доступ.

Вы получаете ошибку incompatible types: Formula is not a functional interface потому что вы не предоставили @FunctionalInterface аннотация, если вы предоставили сообщение об ошибке 'method undefined', компилятор заставит вас создать метод в классе.

@FunctionalInterface должен иметь только один абстрактный метод, который есть в вашем интерфейсе, но в нем отсутствует аннотация.

Но статические методы не имеют такого ограничения, так как мы можем получить к нему доступ без ссылки на объект, как показано ниже.

@FunctionalInterface
public interface Formula {

    double calculate(int a);

    static double sqrt(int a) {
        return Math.sqrt(a);
    }
}

public class Lambda {

    public static void main(String[] args) {
    Formula formula = (a) -> Formula.sqrt(a);
        System.out.println(formula.calculate(100));
    }

}
Другие вопросы по тегам