java 为什么我的lambda得到非法的前向引用,而我的匿名类却没有?[duplicate]

qq24tv8q  于 2023-01-11  发布在  Java
关注(0)|答案(4)|浏览(159)
    • 此问题在此处已有答案**:

Accessing Same Class' enum value inside an enum declaration within a lambda doesn't compile(2个答案)
16小时前关门了。
我正在尝试表示一个State Transition Diagram,我想用Java枚举来完成这个任务。我很清楚有很多其他的方法可以用Map<K, V>或者用枚举中的静态初始化块来完成这个任务。但是,我正在尝试理解为什么会出现下面的情况。
这里有一个(极其)简化的例子,说明我正在尝试做的事情。

enum RPS0
{
  
    ROCK(SCISSORS),
    PAPER(ROCK),
    SCISSORS(PAPER);
     
    public final RPS0 winsAgainst;
     
    RPS0(final RPS0 winsAgainst)
    {
        this.winsAgainst = winsAgainst;
    }
}

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

ScratchPad.java:150: error: illegal forward reference
         ROCK(SCISSORS),
              ^

这很好,我接受。尝试手动插入SCISSORS需要Java尝试并设置SCISSORS,这将触发设置PAPER,这将触发设置ROCK,导致无限循环。我很容易理解为什么这种直接引用是不可接受的,并且由于编译器错误而被禁止。
所以,我尝试着用lambdas做同样的事情。

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();
    }
}

它失败了,基本上是相同的错误。

ScratchPad.java:169: error: illegal forward reference
         ROCK(() -> SCISSORS),
                    ^

我对此有点困扰,因为我真的觉得lambda应该允许它不失败,但不可否认,我对lambda的规则、作用域和边界的理解还不够,因此无法有更坚定的看法。
顺便说一句,我尝试了添加花括号和返回lambda,但这也没有帮助。
所以,我尝试了一个匿名类。

enum RPS2
{
    ROCK
    {
        public RPS2 winsAgainst()
        {
            return SCISSORS;
        } 
    },
         
    PAPER
    {
        public RPS2 winsAgainst()
        {
            return ROCK;
        }     
    },
         
    SCISSORS
    {
        public RPS2 winsAgainst()
        {
            return PAPER;
        }
    };
         
    public abstract RPS2 winsAgainst();   
}

令人震惊的是,它起作用了。

System.out.println(RPS2.ROCK.winsAgainst()); //returns "SCISSORS"

于是,我想在Java Language Specification for Java 19中搜索答案,但我的搜索结果是什么也没有返回。(不区分大小写)对于像"非法"、"转发"、"引用"、"枚举"、"λ"、""、"****"、"*****"、"****************************************************************************************************************************************************************"匿名"等等。这里是我搜索的一些链接。也许我错过了一些回答我问题的链接?

  • 匿名类
  • λ
  • 字段引用的限制
  • 枚举常量自引用的限制

他们都没有回答我的问题。有人能帮我理解阻止我使用lambdas但允许匿名类的游戏规则吗?

EDIT-@DidierL指出了一个到another StackOverflow post的链接,它处理类似的事情。我认为这个问题的答案和我的答案是一样的。简而言之,匿名类有自己的"上下文",而lambda没有。因此,当lambda试图获取变量/方法/等的声明时,就像你内联它一样,就像我上面的RPS0示例一样。

这是令人沮丧的,但我认为,以及"迈克尔的回答都回答了我的问题完成。

EDIT 2-添加此片段以供我与@Michael讨论。

enum RPS4
      {
      
         ROCK
         {
            
            public RPS4 winsAgainst()
            {
            
               return SCISSORS;
            }
         
         },
         
         PAPER
         {
         
            public RPS4 winsAgainst()
            {
            
               return ROCK;
               
            }
            
         },
         
         SCISSORS
         {
         
            public RPS4 winsAgainst()
            {
            
               return PAPER;
            
            }
         
         },
         ;
         
         public final RPS4 winsAgainst;
         
         RPS4()
         {
         
            this.winsAgainst = this.winsAgainst();
         
         }
         
         public abstract RPS4 winsAgainst();
      
      }
mctunoxg

mctunoxg1#

我相信这源于JLS所称的"definite assignment"
首先,概述第一个示例中枚举的初始化顺序可能会有所帮助。
1.某个其他类第一次引用枚举类,例如RPS0.ROCK.winsAgainst()
1.“clinit”(调用静态初始化式)
1.枚举常量按声明顺序初始化
1.相当于public static final RPS0 ROCK = new RPS0(SCISSORS);
1.相当于public static final RPS0 PAPER = new RPS0(ROCK);
1.相当于public static final RPS0 SCISSORS = new RPS0(PAPER);
1.枚举类现在已装入
1.表达式RPS0.ROCK是为#1中的引用类计算的
1.在该示例上调用winsAgainst
ROCK行上失败的原因是SCISSORS没有被“明确赋值”,知道了初始化的顺序,我们可以看到它甚至比这更糟,它不仅是“不明确赋值”,而且是“绝对不赋值”,SCISSORS(3.3)的赋值发生在ROCK(3.1)之后。
ROCK试图访问它时,如果编译器允许,SCISSORS将为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;
    }
}

lambda的情况非常相似。值仍然没有被明确赋值。考虑一个枚举构造函数在Supplier上调用get的情况。请回头参考上面的初始化顺序。在下面的示例中,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
    }
}

真的很烦人,因为你知道你 * 没有 * 以那种方式使用供应商,而这是唯一一次它可能还没有被分配。
抽象类工作的原因是因为构造函数中的expression-target现在已经完全消失了。再次回顾初始化的顺序。你应该能够看到对于任何调用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;
    }
}

只要构造函数从不调用Supplier.get(包括构造函数本身调用的任何方法),就可以证明这是安全的。

t9aqgxwy

t9aqgxwy2#

这并不是真正的答案,但是只要用匿名类替换第一个lambda表达式,它就可以工作了。

enum RPS1 {
    ROCK(new Supplier<>() {
        @Override
        public RPS1 get() {
            return 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();
    }
}
mv1qrgav

mv1qrgav3#

https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.3
根据这里的规则,匿名类似乎是可以接受的,因为它是一个“不同的类”。
示例中有几个注解说“ok -发生在不同的类中”

2g32fytz

2g32fytz4#

我不是Maven,所以我可能是错的,但这是我的理解。
一个非法的前向引用意味着你试图在一个变量被定义之前使用它。这就像说"i = 10; int i;"
前两个实际上使用的是传入的变量。RPS 0和RPS 1将一个未知变量SCISSORS赋给一个需要RPS 0/RPS 1变量的字段。这应该是预期的结果。
对于Anonymous类,它必须做一些不同的事情。Java必须重新排序定义,首先定义RPS 2示例,然后示例化它们。

相关问题