Почему мы не можем использовать методы по умолчанию в лямбда-выражениях?
Я читал этот учебник по Java 8, где автор показал код:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
И затем сказал
Невозможно получить доступ к методам по умолчанию из лямбда-выражений. следующий код не компилируется:
Formula formula = (a) -> sqrt( a * 100);
Но он не объяснил, почему это невозможно. Я запустил код, и он дал ошибку,
несовместимые типы: Формула не является функциональным интерфейсом
Итак, почему это невозможно или в чем смысл ошибки? Интерфейс удовлетворяет требованию функционального интерфейса, имеющего один абстрактный метод.
Ответы
Ответ 1
Это более или менее вопрос о сфере видимости. Из JLS
В отличие от кода, появляющегося в объявлениях анонимного класса, значение имена и ключевые слова this
и super
, появляющиеся в лямбда-теле, наряду с доступностью ссылочных объявлений, являются одинаковыми как в окружающем контексте (за исключением того, что параметры лямбда вводят новые имена).
В вашем примере
Formula formula = (a) -> sqrt( a * 100);
область не содержит декларации для имени sqrt
.
Это также намекает на JLS
Практически говоря, для выражения лямбда необычно говорить о себе (либо называть себя рекурсивно, либо вызывать его другие методы), в то время как более распространено желание использовать имена для ссылки к вещам в окружающем классе, которые иначе были бы затенены (this
, toString()
). Если для лямбда-выражения необходимо ссылаться на себя (как будто через this
), ссылку на метод или анонимную Вместо этого следует использовать внутренний класс.
Я думаю, что это могло быть реализовано. Они решили не допускать этого.
Ответ 2
Лямбда-выражения работают совершенно по-другому от анонимных классов в том, что 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();
}
}
и он печатает что-то вроде
[email protected]
[email protected]
Другими словами 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);
}
};
Ответ 3
Это не совсем так. Методы по умолчанию могут использоваться в лямбда-выражениях.
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);
заключается в определении a Formula
, который работает как функциональный интерфейс, непосредственно через выражение лямбда.
Это работает отлично, в приведенном выше примере кода Value value = () -> 5
или с Formula
в качестве интерфейса, например
Formula formula = (a) -> 2 * a * a + 1;
Но
Formula formula = (a) -> sqrt( a * 100);
не удается, поскольку пытается получить доступ к методу (this.
) sqrt
, но не может.
Lambdas в соответствии со спецификацией наследует их сферу действия из своего окружения, что означает, что this
внутри лямбды относится к той же самой вещи, что и непосредственно за ее пределами. И снаружи нет метода sqrt
.
Мое личное объяснение для этого: внутри выражения лямбда это не совсем ясно, какой конкретный функциональный интерфейс лямбда будет "преобразован". Сравнить
interface NotRunnable {
void notRun();
}
private final Runnable r = () -> {
System.out.println("Hello");
};
private final NotRunnable r2 = r::run;
То же самое выражение лямбда может быть "брошено" на несколько типов. Я думаю об этом, как будто у лямбды нет типа. Это специальная функция без функции, которая может использоваться для любого интерфейса с правильными параметрами. Но это ограничение означает, что вы не можете использовать методы будущего типа, потому что вы не можете этого знать.
Ответ 4
Это немного влияет на обсуждение, но я все равно нашел его интересным.
Еще один способ увидеть проблему - подумать об этом с точки зрения саморегуляции лямбда.
Например:
Formula formula = (a) -> formula.sqrt(a * 100);
Казалось бы, это должно иметь смысл, так как к тому времени, когда лямбда будет выполнена, ссылка formula
должна быть уже инициализирована (т.е. нет способа сделать formula.apply()
до тех пор, пока formula
не будет правильно, в чьем случае, из тела лямбда, тела apply
, должно быть возможно ссылаться на одну и ту же переменную).
Однако это тоже не работает. Интересно, что это было возможно в начале. Вы можете видеть, что Морис Нафталин зарегистрировал его на своем Lambda часто задаваемом веб-сайте. Но по какой-то причине поддержка этой функции была в конечном итоге удалена.
Некоторые предложения, приведенные в других ответах на этот вопрос, уже упоминались там в самом обсуждении в списке рассылки лямбда.
Ответ 5
Доступ к методам по умолчанию можно получить только с помощью ссылок на объекты, если вы хотите получить доступ к методу по умолчанию, который у вас будет иметь ссылку на объект функционального интерфейса, в теле метода выражения лямбда вы не сможете так получить доступ к нему.
Вы получаете сообщение об ошибке incompatible types: Formula is not a functional interface
, потому что вы не указали аннотацию @FunctionalInterface
, если вы предоставили вам возможность получить ошибку "метод 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));
}
}