lambda表达式中使用的变量应为final或有效为final


134

lambda表达式中使用的变量应为final或有效为final

当我尝试使用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;
}

5
您无法通过calTzlambda进行修改。
艾略特·弗里施

2
我以为这是Java 8未能及时完成的​​事情之一。但是Java 8是2014年。Scala和Kotlin允许这样做已经很多年了,因此这很可能实现。Java是否计划消除这种奇怪的限制?
GlenPeterson

5
是@MSDousti的评论的更新链接。
geisterfurz007 '18

我认为您可以使用Completable Futures作为解决方法。
克劳因

我观察到的一件重要事情-您可以使用静态变量代替普通变量(我想这实际上是最终变量)
kaushalpranav

Answers:


68

final可变装置,它可以被实例化一次。在Java中,您不能在lambda以及匿名内部类中使用非最终变量。

您可以使用旧的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为返回类型?

希望这些技巧也能帮助您改善。


我们可以使用非最终静态变量
Narendra Jaggi,

92

尽管其他答案证明了该要求,但它们没有解释为什么存在该要求。

JLS在§15.27.2中提到了原因:

对有效最终变量的限制禁止访问动态变化的局部变量,因为局部变量的捕获可能会引入并发问题。

为了降低错误的风险,他们决定确保捕获的变量永远不会发生突变。


10
好的答案+1,令我感到惊讶的是,最终有效决赛的理由似乎很少。值得注意的是:一个局部变量只能由一个lambda捕捉如果绝对是拉姆达的身体之前分配。这两个要求似乎都确保访问局部变量是线程安全的。
Tim Biegeleisen

2
任何想法为什么这只限于局部变量,而不是类成员?我发现自己经常通过将我的变量声明为班级成员来规避问题……
David Refaeli

4
@DavidRefaeli类成员受内存模型覆盖/影响,如果遵循该模型,则共享时将产生可预测的结果。正如§17.4.1–
Dioxin

这是一个愚蠢的hack,应该删除。编译器应警告潜在的跨线程变量访问,但应允许它。或者,应该足够聪明以知道您的lambda是在同一线程上运行还是在并行运行等。这是一个愚蠢的限制,这让我很难过。就像其他人提到的那样,该问题在C#中不存在。
Josh M.

@JoshM。C#还允许您创建可变的值类型,人们建议避免使用这种类型的值以防止出现问题。Java没有遵循这些原则,而是决定完全阻止它。它确实以灵活性为代价减少了用户错误。我不同意这一限制,但这是合理的。考虑到并行性,将需要在编译器端进行一些额外的工作,这可能就是为什么不采用“ 警告跨线程访问 ” 路由的原因。开发该规范的开发人员可能是我们对此的唯一确认。
二恶英

57

从lambda中,您无法获得所有非最终参考。您需要从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();
}   

我在考虑相同或相似的方法-但我想在这个答案上看到一些专家的建议/反馈吗?
YoYo

4
此代码缺少首字母reference.set(calTz);或必须使用创建引用new AtomicReference<>(calTz),否则作为参数提供的非null的TimeZone将丢失。
朱利安·克朗格

7
这应该是第一个答案。AtomicReference(或类似的Atomic___类)在每种可能的情况下都可以安全地解决此限制。
GlenPeterson

1
同意,这应该是公认的答案。其他答案给出了有关如何退回到非功能性编程模型以及为什么这样做的有用信息,但实际上并没有告诉您如何解决该问题!
乔纳森·本

2
@GlenPeterson也是一个可怕的决定,不仅这种方式要慢得多,而且您也忽略了文档规定的副作用属性。
尤金(Eugene)

41

Java 8具有一个称为“有效最终”变量的新概念。这意味着其值在初始化后不会改变的非最终局部变量称为“有效最终”。

之所以引入这个概念,是因为在Java 8之前,我们不能在匿名类中使用非最终局部变量。如果您想访问匿名类中的局部变量,则必须将其定型。

当引入lambda时,此限制得以缓解。因此,一旦将其初始化为lambda本身,则不更改局部变量就需要将其定为final,这只是一个匿名类。

Java 8意识到每次开发人员使用lambda时都要声明局部变量fi​​nal的痛苦,引入了这一概念,并且不必将局部变量声明为final。因此,如果您看到匿名类的规则没有改变,那就是您不必final每次使用lambda时都编写关键字。

我在这里找到了很好的解释


代码格式仅应用于代码,而不是一般的技术术语。effectively final不是代码,而是术语。请参阅何时将代码格式用于非代码文本?关于元堆栈溢出
查尔斯·达菲,

(因此,“ final关键字”是一个代码字,可以正确地格式化,但是当您使用“最终”而不是代码来描述性地使用时,它是术语。)
查尔斯·达菲,

9

在您的示例中,您可以forEach通过简单的for循环将替换为lamdba 并随意修改任何变量。或者,也许可以重构代码,以使您不需要修改任何变量。但是,我将为完整起见解释错误的含义以及如何解决该错误。

Java 8语言规范§15.27.2

使用但未在lambda表达式中声明的任何局部变量,形式参数或异常参数必须声明为final或有效地声明为final(第4.12.4节),否则在尝试使用时会发生编译时错误。

基本上,您不能calTz在lambda(或本地/匿名类)中修改本地变量(在这种情况下)。要在Java中实现此目的,您必须使用可变对象,并通过lambda对其进行修改(通过最终变量)。可变对象的一个​​示例是一个元素的数组:

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结果= new MyObj(); ...; result.timeZone = ...; ....; 返回result.timezone; 但是请注意,如上所述,这使您面临线程安全问题。请参阅stackoverflow.com/a/50341404/7092558
Gibezynu Nu

0

如果不需要修改变量,那么对于这种问题,一般的解决方法是 提取使用lambda的代码部分,并在method-parameter上使用final关键字。


0

lambda表达式中使用的变量应该是final或有效的final,但是您可以将值分配给final一个元素数组。

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

这与亚历山大·乌达洛夫(Alexander Udalov)的建议非常相似。除此之外,我认为这种方法依赖于副作用。
Scratte
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.