Java lambdas имеют разные переменные требования, чем анонимные внутренние классы
У меня есть анонимный внутренний класс и эквивалентная лямбда. Почему правила инициализации переменных более строгие для лямбда, и есть ли решение более чистое, чем анонимный внутренний класс или инициализация его в конструкторе?
import java.util.concurrent.Callable;
public class Immutable {
private final int val;
public Immutable(int val) { this.val = val; }
// Works fine
private final Callable<String> anonInnerGetValString = new Callable<String>() {
@Override
public String call() throws Exception {
return String.valueOf(val);
}
};
// Doesn't compile; "Variable 'val' might not have been initialized"
private final Callable<String> lambdaGetValString = () -> String.valueOf(val);
}
Изменить: я столкнулся с одним обходным путем: используя getter для val
.
Ответы
Ответ 1
В главе тела выражения лямбда указано
В отличие от кода, появляющегося в объявлениях анонимного класса, значение имена и ключевые слова this
и super
, появляющиеся в лямбда-теле, наряду с доступностью ссылочных объявлений, являются одинаковыми как в окружающем контексте (за исключением того, что параметры лямбда вводят новые имена).
Прозрачность this
(как явная, так и неявная) в теле лямбда-выражения - то есть, рассматривая его так же, как в окружающий контекст - обеспечивает большую гибкость для реализаций и препятствует тому, чтобы значение неквалифицированных имен в теле было в зависимости от разрешения перегрузки.
Они более строгие из-за этого.
Окружающий контекст в этом случае является назначением для поля, а проблема под рукой - это доступ к полю, val
, пустое поле final
в правой части выражения.
Спецификация языка Java содержит
Каждая локальная переменная (§14.4) и каждое пустое поле final
(§4.12.4, §8.3.1.2) должен иметь определенно присвоенное значение, когда любой доступ к его значение имеет значение.
Доступ к его значению состоит из простого имени переменной (или для поля, простое имя поля, имеющего квалификацию this
) происходящее где угодно в выражении, кроме как левый операнд простой оператор присваивания =
(§15.26.1).
Для каждого доступа локальной переменной или пустого поля final
x
, x
должно быть определенно назначенный перед доступом, или возникает ошибка времени компиляции.
Далее далее
Пусть C
- класс, а V
- пустое поле final
не static
из C
, объявленного в C
. Тогда:
-
V
определенно не назначен (и, кроме того, определенно не назначен) перед самым левым инициализатором экземпляра (§8.6) или переменной экземпляра инициализатор C
.
-
V
назначается [un] перед инициализатором экземпляра или инициализатором переменной экземпляра C
, кроме самого левого iff V
. [un], назначенный после инициализатора или экземпляра предыдущего экземпляра переменный инициализатор C
.
Ваш код в основном выглядит следующим образом:
private final int val;
// leftmost instance variable initializer, val still unassigned
private final Callable<String> anonInnerGetValString = ...
// still unassigned after preceding variable initializer
private final Callable<String> lambdaGetValString = ...
Поэтому компилятор определяет, что val
не назначен, когда он получает доступ в выражении инициализации для lambdaGetValString
.
Приведенные выше правила применяются к использованию простого имени val
, а не к квалифицированному выражению this.val
. Вы можете использовать
final Callable<String> lambdaGetValString = () -> String.valueOf(this.val);
Ответ 2
Это не скомпилируется:
public class Example
{
private final int x;
private final int y = 2 * x;
public Example() {
x = 10;
}
}
но это будет:
public class Example
{
private final int x;
private final int y;
public Example() {
x = 10;
y = 2 * x;
}
}
и так будет:
public class Example
{
private final int x = 10;
private final int y = 2 * x;
}
Так что это не связано с лямбдами.
Поле, которое инициализируется в той же строке, в которой оно объявлено, вычисляется до выполнения конструктора.
Поэтому в этот момент переменная "val" (или в этом примере "x" ) не была инициализирована.
Ответ 3
В моем случае у меня был Predicate
, который пытался получить доступ к переменной экземпляра private final
. Я также сделал финал Predicate
, который исправил его.
До - ошибка компилятора, this.availableCities, возможно, не были инициализированы
class Service {
private final List<String> availableCities;
Service(List<String> availableCities) {
this.availableCities = availableCities;
}
private Predicate<String> isCityAvailable = city -> this.availableCities.contains(city);
}
После - больше ошибок нет
class Service {
private final List<String> availableCities;
private final Predicate<String> isCityAvailable;
Service(List<String> availableCities) {
this.availableCities = availableCities;
this.isCityAvailable = city -> this.availableCities.contains(city);
}
}
Я думаю, что ошибка компилятора "возможно, не инициализирована" имеет некоторые достоинства, поскольку в противном случае ничто не мешает вам вызвать Predicate
из конструктора до инициализации последней переменной экземпляра:
Потенциально, почему компилятор обеспечивает это
class Service {
private final List<String> availableCities;
Service(List<String> availableCities, String topCity) {
boolean isTopCityAvailable = isCityAvailable.test(topCity); // Error: this.availableCities is not initialized yet
this.availableCities = availableCities;
}
private Predicate<String> isCityAvailable = city -> this.availableCities.contains(city);
}