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)
我相信这源于 JLS 所谓的“明确分配”。
首先,概述第一个示例的枚举初始化顺序可能会有所帮助。
RPS0.ROCK.winsAgainst()public static final RPS0 ROCK = new RPS0(SCISSORS);public static final RPS0 PAPER = new RPS0(ROCK);public static final RPS0 SCISSORS = new RPS0(PAPER);RPS0.ROCK针对 #1 中的引用类计算表达式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(包括构造函数本身调用的任何方法)。