为啥 java lambda 表达式没有引入新的范围?

Posted

技术标签:

【中文标题】为啥 java lambda 表达式没有引入新的范围?【英文标题】:Why do java lambda expressions not introduce a new level of scope?为什么 java lambda 表达式没有引入新的范围? 【发布时间】:2016-08-24 20:22:28 【问题描述】:

据我了解,在 Haskell 等语言中,以及作为 lambda 演算的一部分,每个 lambda 表达式都有自己的范围,所以如果我有嵌套的 lambda 表达式,例如:\x -> (\x -> x),那么第一个 \x 参数不同于第二个\x

在 Java 中,如果你这样做,你会得到一个编译错误,就像你再次使用 x 作为参数名称或 lambda 中的局部变量名称,如果它已经在封闭范围内使用,例如作为方法参数。

有谁知道为什么 Java 以这种方式实现 lambda 表达式 - 为什么不让它们引入一个新级别的范围并像匿名类一样表现?我假设这是因为一些限制或优化,或者可能是因为 lambdas 必须被入侵到现有语言中?

【问题讨论】:

在此类语言中,您如何引用嵌套 lambda 中的外部 x? @SotiriosDelimanolis 你不一定必须能够,这是一个设计决定。你可以G。也不要从匿名类访问局部变量 x,在该类中定义另一个完全有效的 x。 Variable is already defined in method lambda的可能重复 【参考方案1】:

这与 Java 中其他代码块的行为相同。

这会导致编译错误

int a;

    int a;

虽然没有


    int a;


    int a;

您可以在section 6.4 of the JLS 中阅读有关此主题的信息,并附上一些推理。

【讨论】:

【参考方案2】:

一个 lambda 块一个新块,也就是作用域,但它不像匿名类实现那样建立新的上下文/级别。

来自 Java 语言规范15.27.2 Lambda Body:

与出现在匿名类声明中的代码不同,名称的含义以及出现在 lambda 主体中的 thissuper 关键字,以及引用声明的可访问性,与周围上下文中的 相同(除了 lambda 参数引入了新名称)。

来自 JLS 6.4 Shadowing and Obscuring:

这些规则允许重新声明变量或本地类嵌套类声明(本地类 (§14.3) 和 出现在变量或本地类范围内的匿名类 (§15.9))。因此,形参、局部变量或局部类的声明可能会隐藏在嵌套在方法、构造函数或 lambda 表达式中的类声明中;并且异常参数的声明可能会隐藏在嵌套在 catch 子句的 Block 中的类声明中。

有两种设计方案可用于处理由 lambda 参数和 lambda 表达式中声明的其他变量创建的名称冲突。一种是模仿类声明:与局部类一样,lambda 表达式为名称引入了一个新的“级别”,并且可以重新声明表达式之外的所有变量名称。另一种是“局部”策略:与 catch 子句、for 循环和块一样,lambda 表达式在与封闭上下文相同的“级别”上运行,表达式之外的局部变量不能被遮蔽。 以上规则使用本地策略;没有特殊规定允许在 lambda 表达式中声明的变量隐藏在封闭方法中声明的变量。

例子:

class Test 
    private int f;
    public void test() 
        int a;
        a = this.f;     // VALID
        
            int a;      // ERROR: Duplicate local variable a
            a = this.f; // VALID
        
        Runnable r1 = new Runnable() 
            @Override
            public void run() 
                int a;           // VALID (new context)
                a = this.f;      // ERROR: f cannot be resolved or is not a field
                                 //   (this refers to the instance of Runnable)
                a = Test.this.f; // VALID
            
        ;
        Runnable r2 = () -> 
            int a;      // ERROR: Lambda expression's local variable a cannot redeclare another local variable defined in an enclosing scope.
            a = this.f; // VALID
        ;
    

【讨论】:

那么他们为什么做出这个决定,是否存在意味着他们不能这样做的限制,或者他们不这样做的好处,即每个 lambda 没有新的范围?【参考方案3】:

Java 中的 Lambda 引入了一个新的范围 - 在 lambda 中声明的任何变量都只能在 lambda 内访问。

您真正要问的是 shadowing - 更改已绑定在某个外部范围内的变量的绑定。

允许某种程度的遮蔽是合乎逻辑的:您希望能够通过局部名称来遮蔽全局名称,因为否则您可以通过向某个全局命名空间添加新名称来破坏局部代码。许多语言,为了简单起见,只是将此规则扩展到本地名称。

另一方面,重新绑定本地名称是一种代码异味,可能会导致细微错误,同时 - 同时 - 没有提供任何技术优势。既然提到了Haskell,可以看this discussion on Lambda the Ultimate。

这就是为什么 Java 不允许隐藏局部变量(就像许多其他潜在危险的事情一样),但允许通过局部变量隐藏属性(这样添加属性永远不会破坏已经使用该名称的方法)。

因此,Java 8 的设计者必须回答一个问题,即 lambda 是否应该表现得更像代码块(无阴影)或内部类(阴影),并有意识地决定像前者一样对待它们。

【讨论】:

【参考方案4】:

虽然其他答案看起来像是语言设计者的明确决定,但实际上有一个 JEP 提议为 lambda 参数引入阴影(强调我的):

Lambda 参数不允许隐藏在封闭变量中 范围。 [...] 最好取消此限制,并且 允许 lambda 参数(以及用 lambda 声明的局部变量)隐藏 在封闭范围内定义的变量。

该提案相对较旧,显然还没有进入 JDK。但由于它还包括对下划线的更好处理(在 Java 8 中它被弃用为标识符,以便为这种处理铺平道路),我可以想象整个提案并没有完全脱离桌面。

【讨论】:

以上是关于为啥 java lambda 表达式没有引入新的范围?的主要内容,如果未能解决你的问题,请参考以下文章

Java8——Lambda表达式

函数式编程与Lambda表达式(上)

Java多线程学习笔记— “Lambda表达式”

Java多线程学习笔记— “Lambda表达式”

Lambda表达式

Java编程的逻辑 (91) - Lambda表达式