Переменная, используемая в лямбда-выражении, должна быть окончательной или фактически окончательной.

134

Переменная, используемая в лямбда-выражении, должна быть окончательной или фактически окончательной.

Когда я пытаюсь использовать, calTzон показывает эту ошибку.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            VTimeZone v = (VTimeZone) component;
            v.getTimeZoneId();
            if (calTz == null) {
                calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
user3610470
источник
5
Вы не можете изменить calTzлямбда.
Эллиотт Фриш
2
Я предположил, что это была одна из тех вещей, которые просто не были выполнены вовремя для Java 8. Но Java 8 была 2014 года. Scala и Kotlin допускали это в течение многих лет, так что это, очевидно, возможно. Планирует ли Java когда-нибудь устранить это странное ограничение?
GlenPeterson
5
Вот обновленная ссылка на комментарий @MSDousti.
geisterfurz007
Я думаю, вы могли бы использовать Completable Futures в качестве обходного пути.
Kraulain
Одна важная вещь, которую я заметил - вы можете использовать статические переменные вместо обычных (я думаю, это делает его фактически окончательным)
kaushalpranav

Ответы:

68

А finalпеременная означает , что он может быть создан только один раз. в Java нельзя использовать неокончательные переменные в лямбда-выражениях, а также в анонимных внутренних классах.

Вы можете реорганизовать свой код с помощью старого цикла for-each:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    try {
        for(Component component : cal.getComponents().getComponents("VTIMEZONE")) {
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(calTz==null) {
               calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
           }
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

Даже если я не понимаю некоторых частей этого кода:

  • вы вызываете a v.getTimeZoneId();без использования его возвращаемого значения
  • с назначением calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());вы не изменяете первоначально переданное calTzи не используете его в этом методе
  • Вы всегда возвращаетесь null, почему бы вам не установить voidвозвращаемый тип?

Надеюсь, эти советы помогут вам стать лучше.

Франческо Питцалис
источник
мы можем использовать не конечные статические переменные
Нарендра Джагги
93

Хотя другие ответы подтверждают наличие требования, они не объясняют, почему это требование существует.

JLS упоминает, почему в §15.27.2 :

Ограничение на эффективные конечные переменные запрещает доступ к динамически изменяющимся локальным переменным, захват которых может вызвать проблемы параллелизма.

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

диоксин
источник
11
Хороший ответ +1, и я удивлен тем, насколько мало внимания уделяется причине фактического финала. Примечание: локальная переменная может быть захвачена лямбдой, только если она также определенно назначена перед телом лямбда. Казалось бы, оба требования гарантируют, что доступ к локальной переменной будет потокобезопасным.
Тим Бигелейзен
2
любая идея, почему это ограничено только локальными переменными, а не членами класса? Я часто обхожу эту проблему, объявляя свою переменную членом класса ...
Дэвид Рафаэли
4
Члены класса @DavidRefaeli покрываются / затрагиваются моделью памяти, которая, если следовать ей, даст предсказуемые результаты при совместном использовании. Локальные переменные - нет, как упоминалось в §17.4.1
Диоксин
Это глупый взлом, который следует удалить. Компилятор должен предупреждать о потенциальном доступе к переменным между потоками, но должен его разрешать. Или должен быть достаточно умным, чтобы знать, работает ли ваша лямбда в том же потоке или параллельно и т. Д. Это глупое ограничение, которое меня огорчает. И, как отмечали другие, проблем не существует, например, в C #.
Джош М.
@JoshM. C # также позволяет создавать изменяемые типы значений , которых рекомендуется избегать во избежание проблем. Вместо того чтобы иметь такие принципы, Java решила полностью предотвратить это. Это снижает количество ошибок пользователя за счет гибкости. Я не согласен с этим ограничением, но оно оправдано. Учет параллелизма потребует некоторой дополнительной работы со стороны компилятора, поэтому, вероятно, поэтому не был выбран маршрут « предупреждать о доступе между потоками ». Разработчик, работающий над спецификацией, вероятно, будет нашим единственным подтверждением этого.
Диоксин
59

Из лямбды вы не можете получить ссылку ни на что не окончательное. Вам нужно объявить финальную оболочку извне lamda для хранения вашей переменной.

Я добавил последний объект «ссылка» в качестве этой оболочки.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    final AtomicReference<TimeZone> reference = new AtomicReference<>();

    try {
       cal.getComponents().getComponents("VTIMEZONE").forEach(component->{
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(reference.get()==null) {
               reference.set(TimeZone.getTimeZone(v.getTimeZoneId().getValue()));
           }
           });
    } catch (Exception e) {
        //log.warn("Unable to determine ical timezone", e);
    }
    return reference.get();
}   
DMozzy
источник
Я думал о том же или подобном подходе, но хотел бы получить совет / отзыв какого-нибудь эксперта по этому ответу?
YoYo
4
В этом коде отсутствует начальная буква, reference.set(calTz);или ссылка должна быть создана с использованием new AtomicReference<>(calTz), иначе ненулевой часовой пояс, указанный в качестве параметра, будет потерян.
Жюльен Кронегг
8
Это должен быть первый ответ. AtomicReference (или аналогичный класс Atomic___) безопасно обходит это ограничение во всех возможных обстоятельствах.
GlenPeterson
1
Согласен, это должен быть принятый ответ. Другие ответы дают полезную информацию о том, как вернуться к нефункциональной модели программирования и почему это было сделано, но на самом деле не говорят вам, как обойти проблему!
Джонатан Бенн
2
@GlenPeterson, и это тоже ужасное решение, не только это намного медленнее, но вы также игнорируете свойство побочных эффектов, которое требует документация.
Евгений
41

В Java 8 появилась новая концепция переменной, которая называется «эффективно конечная». Это означает, что неконечная локальная переменная, значение которой никогда не изменяется после инициализации, называется «Фактически конечной».

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

Когда была введена лямбда, это ограничение было снято. Следовательно, необходимо сделать локальную переменную окончательной, если она не изменилась после инициализации, поскольку лямбда сама по себе является не чем иным, как анонимным классом.

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

Я нашел хорошее объяснение здесь

Динеш Арора
источник
Форматирование кода следует использовать только для кода , а не для технических терминов в целом. effectively finalэто не код, это терминология. См. Раздел Когда следует использовать форматирование кода для текста, не являющегося кодом? о переполнении мета-стека .
Чарльз Даффи
(Таким образом, « finalключевое слово» - это слово кода и его можно правильно отформатировать таким образом, но когда вы используете «final» описательно, а не как код, вместо этого используется терминология).
Чарльз Даффи
9

В вашем примере вы можете заменить на forEachlamdba простой forцикл и свободно изменять любую переменную. Или, возможно, реорганизуйте свой код, чтобы вам не нужно было изменять какие-либо переменные. Однако я объясню для полноты, что означает ошибка и как ее обойти.

Спецификация языка Java 8, §15.27.2 :

Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные в лямбда-выражении, должны быть либо объявлены окончательными, либо быть фактически окончательными ( §4.12.4 ), либо при попытке использования возникает ошибка времени компиляции.

По сути, вы не можете изменить локальную переменную ( calTzв этом случае) из лямбда (или локального / анонимного класса). Чтобы добиться этого в Java, вы должны использовать изменяемый объект и изменить его (через конечную переменную) с помощью лямбда. Одним из примеров изменяемого объекта здесь может быть массив из одного элемента:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    TimeZone[] result = { null };
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            ...
            result[0] = ...;
            ...
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return result[0];
}
Александр Удалов
источник
Другой способ - использовать поле объекта. Например, MyObj result = new MyObj (); ...; result.timeZone = ...; ....; вернуть result.timezone; Обратите внимание, что, как объяснялось выше, это создает проблемы с безопасностью потоков. См stackoverflow.com/a/50341404/7092558
Gibezynu Nu
0

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

robie2011
источник
0

Переменная, используемая в лямбда-выражении, должна быть окончательной или фактически окончательной, но вы можете присвоить значение последнему массиву из одного элемента.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        TimeZone calTzLocal[] = new TimeZone[1];
        calTzLocal[0] = calTz;
        cal.getComponents().get("VTIMEZONE").forEach(component -> {
            TimeZone v = component;
            v.getTimeZoneId();
            if (calTzLocal[0] == null) {
                calTzLocal[0] = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
Андреас Фотеас
источник
Это очень похоже на предложение Александра Удалова. Кроме того, я думаю, что этот подход основан на побочных эффектах.
Scratte,