Почему мы не можем использовать методы по умолчанию в лямбда-выражениях?
Я читал этот учебник по 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));
}
}