Java 8 catch 22 с лямбда-выражениями и эффективно конечный

Я играю с Java 8 и нахожу базовый сценарий, который иллюстрирует catch 22, где исправление одной ошибки компиляции вызывает другую ошибку компиляции. Сценарий (это просто пример, упрощенный от чего-то более сложного):

public static List<String> catch22(List<String> input) {
    List<String> result = null;
    if (input != null) {
      result = new ArrayList<>(input.size());
      input.forEach(e -> result.add(e)); // compile error here
    }

    return result;
}

Я получаю ошибку компиляции:

Локальная переменная, определенная в охватывающей области, должна быть окончательной или окончательной окончательной

Если я изменил первую строку на:

List<String> result;

Я получаю ошибку компиляции в последней строке:

Результат локальной переменной не может быть инициализирован

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

Ответ 1

Ошибка возникает, потому что ваш result список не эффективен final, который является требованием для его использования в лямбда. Один из вариантов - объявить переменную внутри условия if и return null; снаружи. Но я не думаю, что это была бы хорошая идея. Ваш текущий метод не делает ничего полезного. Было бы гораздо разумнее вернуть из него пустой список.

Сказав, что все, я бы сказал, поскольку вы играете с Java 8, используйте Optional вместе с потоками:

public static List<String> catch22(List<String> input) {
    return Optional.ofNullable(input)
            .orElse(new ArrayList<String>())
            .stream().collect(Collectors.toList());
}

И если вы хотите вернуть null, я, вероятно, изменю свой метод на:

public static List<String> catch22(List<String> input) {
    if (input == null) return null;
    return input.stream().collect(Collectors.toList());
    // Or this. B'coz this is really what your code is doing.
    return new ArrayList<>(input);
}

Ответ 2

Вставьте объявление внутри блока с помощью ввода!= null. Пример:

public static List<String> catch22(List<String> input) {
    if (input != null) {
        List<String> result;
        result = new ArrayList<>(input.size());
        input.forEach(e -> result.add(e)); // compile error here
        return result;
    } else {
        return null; // or empty list or something that suits your app
    }
}

Ответ 3

forEach(...) применяет операцию для каждого элемента Stream. Вы действительно этого не хотите, вы хотите Stream "потребитель", который производит один вывод List<String>.

К счастью, это считается Collector в текущей структуре, а Collectors.toList() делает именно то, что вы хотите.

List<String> duplicate = input.stream().collect(Collectors.toList());

Ответ 4

Сделав окончательный результат и поместив нулевое присвоение в блок else, вы сможете сохранить текущую структуру вашего метода и "решить" ваш catch22.

public static List<String> catch22(List<String> input) {
    final List<String> result;
    if (input != null) {
        result = new ArrayList<>(input.size());
        input.forEach(e -> result.add(e));
    } else {
        result = null;
    }

    return result;
}

Ответ 5

Вы можете сделать это

public static List<String> catch22(List<String> input) {
List<String> result = null;
if (input != null) {
  result = new ArrayList<>(input.size());
  List<String> effectivelyFinalResult = result;
  input.forEach(e -> effectivelyFinalResult.add(e)); // No compile error here
}

return result;
}

чтобы обойти его.

Ответ 6

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

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

Даже возможность считывать значение изменяемых локальных переменных из другого потока может привести к необходимости синхронизации или использования летучих, чтобы избежать чтения устаревших данных.

Ответ 7

Здесь я обобщил некоторые общие решения для lambdas выражений, модифицирующих локальные переменные метода.

Lambdas Выражения фактически являются анонимными внутренними классами в краткой форме. Когда мы используем их в локальных методах, мы должны учитывать несколько ограничений:

  • выражения lambdas (анонимный внутренний класс) не могут изменять локальные переменные окружающего метода ссылка здесь, они должны быть окончательными или в java 8 окончательно

  • В отличие от переменных экземпляра локальные переменные не получают значений по умолчанию, и если вы хотите их использовать или вернуть, вы должны сначала их инициализировать

Когда эти два ограничения сталкиваются (в случае определения lambdas в методе, где lambdas модифицирует локальную переменную в окружающем методе), мы попадаем в проблему

решения:

решение 1: не изменять локальную переменную метода в lambdas или вместо этого использовать переменные экземпляра

solution 2: выполните некоторые трюки, например скопируйте локальную переменную в другую и передайте это lambdas:

public static List<String> catch22(List<String> input) {
 List<String> result = null;
 if (input != null) {
   result = new ArrayList<>(input.size());
   List<String> effectivelyFinalResult = result;
   input.forEach(e -> effectivelyFinalResult.add(e)); 
 }
 return result;
}

или ограничить область локальных переменных, чтобы вы не попали в проблему, чтобы не инициализировать их:

public static List<String> catch22(List<String> input) {
if (input != null) {
    List<String> result; // result gets its value in the lambdas so it is effectively final
    result = new ArrayList<>(input.size());
    input.forEach(e -> result.add(e));
    return result;
} else {
    return null; 
}
}