为什么Java Lambda表达式不会引入新的作用域级别?

11
据我了解,像Haskell这样的语言以及λ演算中,每个λ表达式都有自己的作用域。因此如果我有嵌套的 λ 表达式,例如:\x -> (\x -> x),那么第一个 \x 参数就不同于第二个 \x

在Java中,如果这样做会导致编译错误,就像如果在lambda中再次使用x作为参数名称或局部变量名称,若它已经在封闭作用域中被使用,比如作为方法参数。
有没有人知道为什么Java会以这种方式实现lambda表达式 - 为什么不让它们引入新的作用域,并像匿名类一样运行呢?我猜想这是因为某些限制或优化,或者可能是因为需要将lambda集成到现有语言中。

在这些语言中,你如何引用嵌套的 lambda 中的外部 x? - Sotirios Delimanolis
1
@SotiriosDelimanolis,你不一定需要这样做,这是一个设计决策。例如,在定义另一个x的匿名类中,您也可以不访问局部变量x,这是完全有效的。 - Vampire
2
可能是Variable is already defined in method lambda的重复问题。 - ZhekaKozlov
4个回答

12

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

这将导致编译错误。

int a;
{
    int a;
}

虽然这并不会

{
    int a;
}
{
    int a;
}

您可以在JLS的第6.4节中阅读相关主题,以及一些推理。


5
一个 Lambda 块是一个新的块,也就是作用域,但它不像匿名类实现一样建立新的上下文/层级。
来自 Java 语言规范 15.27.2 Lambda Body:
与出现在匿名类声明中的代码不同,在 lambda 主体中出现的名称、this 和 super 关键字以及引用声明的可访问性与周围上下文中的相同(除了 lambda 参数引入新名称)。
并且从 JLS 6.4 Shadowing and Obscuring:
这些规则允许在嵌套类声明(本地类(§14.3)和匿名类(§15.9))中重新声明变量或本地类。因此,形式参数、局部变量或本地类的声明可以在方法、构造函数或 lambda 表达式中嵌套的类声明中被遮蔽;在 catch 子句的块内部嵌套的类声明中可能会遮蔽异常参数的声明。
处理由 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
        };
    }
}

1
所以为什么他们做出了这个决定呢?是因为有限制导致他们无法这样做,还是因为不这样做有好处,比如每个lambda都没有新的作用域层级? - Tranquility

2
Java中的Lambda表达式确实引入了一个新的作用域——在Lambda表达式中声明的任何变量只能在该Lambda表达式内部访问。
您真正询问的是“遮蔽”——更改已经绑定在某个外部作用域中的变量的绑定。
允许一定程度的遮蔽是合理的:您希望能够通过本地名称遮蔽全局名称,否则您可能会通过向某个全局命名空间添加新名称来破坏本地代码。为了简单起见,很多语言都将这个规则扩展到本地名称。
另一方面,重新绑定本地名称是一种代码异味,并且可能是微妙错误的源头,同时也不提供任何技术优势。由于您提到了Haskell,您可以查看Lambda the Ultimate上的这个讨论
这就是为什么Java禁止遮蔽本地变量(像许多其他潜在危险的事情一样),但允许用本地变量遮蔽属性(以便添加属性永远不会破坏已经使用该名称的方法)。
因此,Java 8的设计师们不得不回答一个问题,即lambda是否应该更像代码块(无遮蔽)还是像内部类(遮蔽),并做出了将它们视为前者的有意决定。

“不提供任何技术优势”这个说法有点过于绝对了。也许可以用“许多”来替代“任何”。比如,我想要使用lambda参数来高效地模拟动态绑定,但由于阴影限制,我无法实现。此外,阴影限制意味着像a -> a + 1这样的表达式,在其他语言中是封闭的,但在Java中却不是封闭的。如果你想在不同的上下文中使用这个表达式,可能需要将a重命名为其他名称。 - undefined

2

虽然其他答案让人觉得这是语言设计者做出的明确决定,但实际上有一个JEP提议引入lambda参数的遮蔽(重点在于我):

Lambda参数不允许遮蔽封闭作用域中的变量。[...] 解除这个限制是可取的,并且允许lambda参数(以及使用lambda声明的本地变量)遮蔽在封闭作用域中定义的变量。

该提议相对较旧,显然还没有进入JDK。但由于它还包括更好的下划线处理方式(在Java 8中已将其作为标识符弃用,为此处理铺平道路),我可以想象整个提议并未完全被否决。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接