变量已在方法 lambda 中定义

Variable is already defined in method lambda

提问人:ryvantage 提问时间:4/1/2014 最后编辑:ZhekaKozlovryvantage 更新时间:8/10/2019 访问量:25158

问:

考虑以下几乎可以编译的 Java 8 代码:

public static void main(String[] args) {

    LinkedList<User> users = null;
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User user = users.stream().filter((user) -> user.getId() == 1).findAny().get();
}

static class User {

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

您会注意到抛出编译器错误:User user = users.stream().filter((user) -> user.getId() == 1).findAny().get();

变量 user 已在方法 main(String[]) 中定义

我的问题是:为什么 Lambda 表达式将正在初始化的变量与已定义的 Lambda 表达式在同一行上?我知道 Lambda 在自身外部查找(并使用)局部变量,因此您不能将 Lambda 内部使用的变量命名为与外部变量相同的变量。但是,为什么正在定义的变量被认为是已经定义的呢?

Lambda Java-8

评论

0赞 MirroredFate 4/1/2014
声明在初始化之前发生。您的局部变量是在 lambda 表达式中定义的。useruser
0赞 Edwin Dalorzo 4/1/2014
@MirroredFate 我认为关键是 lambda 表达式参数实际上应该在完全不同的上下文(表达式的评估上下文)中使用,那么为什么它应该受到外部定义的变量的影响呢?useruser
0赞 Edwin Dalorzo 4/1/2014
到目前为止,我能想到的唯一论点是该变量是闭包环境的一部分,它已经在 lambda 表达式的上下文中,因此,如果您声明另一个变量,则存在名称冲突。然而,我仍然想知道,为什么它不是简单地被遮蔽。这是否与引擎盖下事物的仪器化方式有关?useruser
0赞 MirroredFate 4/2/2014
@EdwinDalorzo 他的问题是,为什么被定义的变量被认为是已经定义的?,我的评论试图解释这一点。
1赞 Holger 4/2/2014
您混淆了声明和初始化。该变量在初始化之前声明。对于初始化器中的 lambda,变量已声明,但尚未初始化。对于没有 lambda 的局部变量也是如此,例如,第二个变量将引用已经声明的变量,但引用无效,因为此时未初始化。不会尝试在外部作用域中查找已初始化的。userint x=x+1;xxxx

答:

8赞 Mzf 4/1/2014 #1

查看代码

User user = users.stream().filter((user) -> user.getId() == 1).findAny().get();

变量名称为 ,lambda 中的变量也是useruser

尝试将其更改为如下所示

User user = users.stream().filter((otherUser) -> otherUser.getId() == 1).findAny().get();

评论

7赞 fge 4/1/2014
嗯,我认为 OP 已经意识到了这一点
2赞 Edwin Dalorzo 4/1/2014
是的,我认为问题是为什么 lambda 变量不在闭包上下文中遮蔽变量?否则这个问题很无聊。useruser
3赞 Ingo 4/1/2014 #2

它与任何其他局部变量相同:您不允许在更多的内部 {} 块中隐藏它们。

17赞 Sotirios Delimanolis 4/1/2014 #3

让我们去看一下 Java 语言规范中关于名称及其作用域的信息

方法的形式参数的作用域 (§8.4.1),构造函数 (§8.8.1) 或 lambda 表达式 (§15.27) 是 方法、构造函数或 lambda 表达式。

块中局部变量声明的范围 (§14.4) 是 显示声明的块的其余部分,以 自己的初始值设定项,并在 局部变量声明语句。

然后,关于阴影和遮挡的主题

局部变量 (§14.4)、形式参数 (§8.4.1、§15.27.1)、 异常参数 (§14.20) 和本地类 (§14.3) 只能是 指使用简单名称,而不是限定名称(§6.2)。

有些声明不允许在本地范围内进行 变量、形式参数、异常参数或本地类 声明,因为无法区分 仅使用简单名称声明的实体。

如果使用局部变量 v 的名称,则为编译时错误 在 v 范围内声明一个新变量,除非 变量在声明位于 v.

所以,在

User user = users.stream().filter((user) -> user.getId() == 1).findAny().get();

,变量的作用域是该块中它后面的所有内容。现在,您正在尝试使用该变量的名称在作用域内声明一个新变量,但不是user

在声明在 v 范围内的类中。

因此,会发生编译时错误。(它是在 lambda 表达式中声明的,而不是在类中声明的。

-1赞 Thirumalai Parthasarathi 9/25/2017 #4

这个问题很老了,但我认为我的答案可以使已经给出的答案更加清晰。特别是对@Sotirios Delimanolis 的 . 中的 lambda 赋值

    User user = users.stream().filter((user) -> user.getId() == 1).findAny().get();

失败的原因与以下代码失败的原因相同。

    Object e = null;
    try{
      throw new Exception();
    } catch(Exception e) { // compilation fails because of duplicate declaration
      //do nothing
    }

局部变量 (§14.4)、形式参数 (§8.4.1、§15.27.1)、异常参数 (§14.20) 和局部类 (§14.3) 只能使用简单名称引用,而不能使用限定名称 (§6.2) 引用。

某些声明不允许在局部变量、形式参数、异常参数或局部类声明的范围内进行,因为仅使用简单名称无法区分声明的实体

因为 lambda 与上面提到的所有事情具有相同的范围,所以这失败了。

评论

0赞 ryvantage 9/27/2017
“无法区分”我不同意。正在初始化的肯定是不可用的(例如,显然是错误的)。因此,说 lambda 中的变量显然不是指当前正在初始化的变量似乎并不夸张。因此,编译器应该能够授予您使用相同名称的能力。User userBigDecimal x = x.plus(y)user
0赞 Thirumalai Parthasarathi 10/13/2017
从逻辑上讲,你是对的。但是 java 语义在 lambda 或任何声明中都没有使用这种推理。java 编译器无法区分前向引用和 lambda 参数。顺便说一句,无法区分的是 JLS,如果这是你反对的答案。
0赞 ryvantage 10/14/2017
伙计,我知道。这个问题清楚地表明,我知道你不能,但为什么他们特别决定将变量名称的范围如此具体,以排除没有冲突的明显情况,这仍然是一个谜。同样,很明显,他们特别决定制定这个规范,但似乎没有明显的原因。你的回答没有增加任何新内容。唯一剩下的新答案是编译器通过限制您来防止错误的情况。
0赞 ryvantage 10/14/2017
您能想到这样一种情况吗,如果允许您将内部 lambda 变量命名为与正在初始化的变量相同的名称,则会导致运行时错误?
0赞 Thirumalai Parthasarathi 10/16/2017
因为 lambda 只是另一个块,所以它没有自己的上下文。第 15.27.2 节 Lambda 正文指定,与匿名类声明中出现的代码不同,lambda 正文中出现的名称和 this超级关键字的含义,以及引用声明的可访问性,与周围上下文中的含义相同(除了 lambda 参数引入了新名称)。
4赞 ZhekaKozlov 11/9/2017 #5

请注意,此限制将在将来的版本中删除。引用 JEP-302

不允许 Lambda 参数对封闭范围中的变量进行遮蔽。(换句话说,lambda 的行为类似于 for 语句 - 参见 JLS)这通常会导致问题,如以下(非常常见的)情况:

Map<String, Integer> msi = ...
...
String key = computeSomeKey();
msi.computeIfAbsent(key, key -> key.length()) //error

在这里,尝试在 computeIfAbsent 调用中将 name 键重用为 lambda 参数失败,因为已在封闭上下文中定义了具有相同名称的变量。

最好解除此限制,并允许 lambda 参数(以及用 lambda 声明的局部变量)对封闭范围内定义的变量进行影子。(一个可能反对的论点是可读性:如果允许 lambda 参数进行遮蔽,那么在上面的例子中,标识符“key”在使用它的两个地方表示两种不同的东西,并且似乎没有语法障碍来区分这两种用法。

评论

0赞 ryvantage 11/10/2017
很酷。不过,与手头的主题关系稍差一些,因为我示例中的标识符并不意味着两个不同的东西,因为外部变量是由 lambda 在与声明相同的一行中初始化的。换句话说,外部变量只能在 lambda 完成后使用,从而创建上述缺失的语法障碍。