为什么我的 lambda 获得非法前向引用,但我的匿名类却没有?

dav*_*hew 5 java lambda enums anonymous-class circular-reference

我正在尝试表示状态转换图,并且我想使用 Java 枚举来实现此目的。我很清楚,还有许多其他方法可以使用Map<K, V>或可能使用枚举中的静态初始化块来完成此操作。但是,我试图理解为什么会发生以下情况。

这是我正在尝试做的一个(极其)简化的示例。


enum RPS0
{
  
    ROCK(SCISSORS),
    PAPER(ROCK),
    SCISSORS(PAPER);
     
    public final RPS0 winsAgainst;
     
    RPS0(final RPS0 winsAgainst)
    {
        this.winsAgainst = winsAgainst;
    }
}
Run Code Online (Sandbox Code Playgroud)

显然,由于非法的前向引用而失败。

ScratchPad.java:150: error: illegal forward reference
         ROCK(SCISSORS),
              ^
Run Code Online (Sandbox Code Playgroud)

没关系,我接受这一点。尝试手动插入SCISSORS需要Java尝试和设置SCISSORS,然后触发设置PAPER,然后触发设置ROCK,导致无限循环。那么我可以很容易地理解为什么这种直接引用是不可接受的,并且会因编译器错误而被禁止。

因此,我尝试对 lambda 进行同样的操作。

ScratchPad.java:150: error: illegal forward reference
         ROCK(SCISSORS),
              ^
Run Code Online (Sandbox Code Playgroud)

它失败了,并出现基本相同的错误。

ScratchPad.java:169: error: illegal forward reference
         ROCK(() -> SCISSORS),
                    ^
Run Code Online (Sandbox Code Playgroud)

我对此有点困扰,因为我真的觉得 lambda 应该让它不会失败。但不可否认的是,我对 lambda 表达式的规则、范围和边界了解不够,无法形成更坚定的观点。

顺便说一句,我尝试添加花括号并返回 lambda,但这也没有帮助。

所以,我尝试了一个匿名类。

enum RPS1
{
    ROCK(() -> SCISSORS),
    PAPER(() -> ROCK),
    SCISSORS(() -> PAPER);
     
    private final Supplier<RPS1> winsAgainst;
     
    RPS1(final Supplier<RPS1> winsAgainst)
    {
        this.winsAgainst = winsAgainst;
    }
     
    public RPS1 winsAgainst()
    {
        return this.winsAgainst.get();
    }
}
Run Code Online (Sandbox Code Playgroud)

令人震惊的是,它成功了。

System.out.println(RPS2.ROCK.winsAgainst()); //returns "SCISSORS"
Run Code Online (Sandbox Code Playgroud)

于是,我想在Java 19 的 Java 语言规范中搜索答案,但我的搜索最终什么也没返回。我尝试使用 Ctrl+F 搜索(不区分大小写)相关短语,例如“ Illegal ”、“ Forward ”、“ Reference ”、“ Enum ”、“ Lambda ”、“ Anonymous ”等。以下是我搜索到的一些链接。也许我错过了其中一些可以回答我的问题的东西?

他们都没有回答我的问题。有人可以帮助我理解阻止我使用 lambda 但允许匿名类的规则吗?

编辑- @DidierL 指出了另一个处理类似内容的StackOverflow 帖子的链接我认为这个问题的答案和我的答案是一样的。简而言之,匿名类有自己的“上下文”,而 lambda 则没有。因此,当 lambda 尝试获取变量/方法/等的声明时,它与内联执行的操作相同,就像上面的 RPS0 示例一样。

这很令人沮丧,但我认为,@Michael 的回答都已经回答了我的问题。

编辑 2 - 添加此片段用于我与@Michael 的讨论。

ScratchPad.java:169: error: illegal forward reference
         ROCK(() -> SCISSORS),
                    ^
Run Code Online (Sandbox Code Playgroud)

Mic*_*ael 3

我相信这源于 JLS 所谓的“明确分配”

首先,概述第一个示例的枚举初始化顺序可能会有所帮助。

  1. 其他一些类第一次引用枚举类,例如RPS0.ROCK.winsAgainst()
  2. “clinit”(调用静态初始化器)
  3. 枚举常量按声明顺序初始化
    1. 相当于public static final RPS0 ROCK = new RPS0(SCISSORS);
    2. 相当于public static final RPS0 PAPER = new RPS0(ROCK);
    3. 相当于public static final RPS0 SCISSORS = new RPS0(PAPER);
  4. 枚举类现已加载
  5. RPS0.ROCK针对 #1 中的引用类计算表达式
  6. winsAgainst在该实例上调用。

线上失败的原因ROCK是因为SCISSORS没有“明确分配”。知道初始化的顺序,我们可以看到它甚至比这更糟糕。不但没有明确分配,而且绝对没有分配。(3.3)的赋值SCISSORS发生在ROCK(3.1) 之后。

SCISSORSROCK如果编译器允许的话,当尝试访问它时将为 null 。稍后分配。

如果我们引入一些间接的方法,我们就可以亲眼看到这一点。明确的赋值问题现在已经消失了,因为我们的构造函数没有直接引用字段。编译器不会检查构造函数表达式中的任何明确赋值。构造函数使用方法调用的结果,而不是字段。

我们所做的只是欺骗编译器允许一些会失败的事情。

enum RPS0
{
    ROCK(scissors()),
    PAPER(rock()),
    SCISSORS(paper());

    public final RPS0 winsAgainst;

    RPS0(final RPS0 winsAgainst)
    {
        this.winsAgainst = Objects.requireNonNull(winsAgainst); // boom
    }
    
    
    
    private static RPS0 scissors() {
        return RPS0.SCISSORS;
    }

    private static RPS0 rock() {
        return RPS0.ROCK;
    }

    private static RPS0 paper() {
        return RPS0.PAPER;
    }
}
Run Code Online (Sandbox Code Playgroud)

lambda 的情况几乎相同。该值仍然没有明确指定。考虑枚举构造函数get调用Supplier. 参考上面的初始化顺序。在下面的示例中,ROCK将尝试SCISSORS在初始化之前进行访问,这是编译器试图保护您免受潜在错误的影响。

enum RPS1
{
    ROCK(() -> SCISSORS), // compiler error
    PAPER(() -> ROCK),
    SCISSORS(() -> PAPER);
     
    private final Supplier<RPS1> winsAgainst;
     
    RPS1(final Supplier<RPS1> winsAgainst)
    {
        RPS1.get(); // doesn't compile, but would be null if it did
    }
}
Run Code Online (Sandbox Code Playgroud)

确实很烦人,因为您知道您没有这种方式使用供应商,并且这是唯一一次它可能尚未分配。

抽象类起作用的原因是构造函数中的表达式目标现在完全消失了。再次参考初始化的顺序。您应该能够看到,对于任何调用 的内容winsAgainst,例如 #1 中的示例,该调用 (#6) 必然在所有枚举常量已初始化 (#3) 之后发生。编译器可以保证此访问是安全的。

将我们知道的两件事放在一起 - 我们可以使用间接来阻止编译器抱怨缺乏明确的赋值,并且Supplier可以惰性地提供值 - 我们可以创建一个替代解决方案:

enum RPS0
{
    ROCK(RPS0::scissors), // i.e. () -> scissors()
    PAPER(RPS0::rock),
    SCISSORS(RPS0::paper);

    public final Supplier<RPS0> winsAgainst;

    RPS0(Supplier<RPS0> winsAgainst) {
        this.winsAgainst = winsAgainst;
    }
    
    public RPS0 winsAgainst() {
        return winsAgainst.get();
    }


    // Private indirection methods
    private static RPS0 scissors() {
        return RPS0.SCISSORS;
    }

    private static RPS0 rock() {
        return RPS0.ROCK;
    }

    private static RPS0 paper() {
        return RPS0.PAPER;
    }
}
Run Code Online (Sandbox Code Playgroud)

这被证明是安全的,前提是构造函数从不调用Supplier.get(包括构造函数本身调用的任何方法)。

  • “这实际上与 lambda 或匿名类没有直接关系” - 我认为它确实与“类”直接相关。一旦您使用不同的(无论是匿名的还是其他的)类,非法的前向引用就会消失。据我所知,Lambda 表达式不是类。 (2认同)
  • @davidalayachew 是的,RPS4 仍然失败。将字段初始值设定项更改为 `this.winsAgainst = Objects.requireNonNull(this.winsAgainst());` 它会爆炸。值得注意的是,该字段在这里并没有增加任何价值。每次访问该字段时,您只需调用“winsAgainst()”即可,然后它就会起作用。 (2认同)