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);
}