【问题标题】:Why does my lambda get illegal forward reference, but my anonymous class does not? [duplicate]为什么我的 lambda 得到非法的前向引用,而我的匿名类却没有? [复制]
【发布时间】:2023-01-11 03:28:18
【问题描述】:

我正在尝试表示 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,导致无限循环。我很容易理解为什么这种直接引用是不可接受的,并且由于编译器错误而被禁止。

因此,我尝试并尝试对 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();
    }
}

它因基本相同的错误而失败。

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 寻找答案,但我的搜索结果一无所获。我尝试对相关短语进行 Ctrl+F 搜索(不区分大小写),例如“非法的", "向前", "参考", "枚举", "拉姆达", "匿名的“等等。这是我搜索的一些链接。也许我错过了其中可以回答我问题的内容?

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

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

这令人沮丧,但我认为,以及@Michael 的回答都已经完整地回答了我的问题。

编辑 2- 添加这个 sn-p 用于我与@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();
      
      }
   

【问题讨论】:

  • 有趣的实验。 jenkov.com/tutorials/java/lambda-expressions.html 声明“Java lambda 表达式只能在它们匹配的类型是单个方法接口的情况下使用”。所以看起来你尝试应用 lambda 的地方不是应用它的好地方。
  • @ZackMacomber 感谢您的回复。我不确定你是否正确。我匹配的接口不应该是我的Supplier&lt;RPS1&gt;吗?
  • 很好的问题,但为简洁起见,我对其进行了编辑。我不认为你的(不幸的是没有结果)搜索真的增加了很多,我认为没有他们这是一个更好的问题。如果您强烈不同意,请随时将其添加回去,但也许可以编辑到要点。
  • @Michael 我看到了您的编辑。感谢您所做的更改。我做了一个简单的项目符号列表,列出了我尝试进行的搜索。这应该满足简洁的要求,同时让人们的支持更加知情/更有针对性。如果您觉得应该有所不同,请编辑我的编辑。

标签: java lambda enums circular-reference anonymous-class


【解决方案1】:

我相信这源于 JLS 所谓的"definite assignment"

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

  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 不是“明确分配”。知道初始化的顺序,我们可以看到它比这更糟糕。不仅是不一定赋值,就是当然不分配。 SCISSORS (3.3) 的赋值发生在 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;
          }
      }
      

      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
          }
      }
      

      很烦人,真的,因为你知道你是不是以这种方式使用 Supplier,这是唯一一次它可能还没有被分配。

      抽象类起作用的原因是构造函数中的表达式目标现在完全消失了。再一次,回到初始化的顺序。你应该能够看到任何调用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(包括构造函数本身调用的任何方法),这可以证明是安全的。

【讨论】:

  • 感谢您的答复。我不知道绑定这个词是否正确,但它确实为我指明了正确的方向。为了确保我理解,听起来您是在说这里真正的问题是 Java 尝试“绑定”lambda 比它对匿名类更早,从而导致出现此问题?
  • “这真的与 lambda 或匿名类没有直接关系”——我认为它确实与“类”有直接关系。一旦您使用不同的(无论是匿名的还是其他的)类,非法的前向引用就会消失。据我所知,Lambda 表达式不是类。
  • @Michael 愚蠢的我,我应该在评论之前运行它。我现在明白你的意思了。谢谢你。
  • @Michael 感谢您耐心帮助我理解这一点。作用域以及如何获取变量是我对 Java 从未真正了解的事情之一。我花了很长时间才了解 lambda 以及它们如何在有效最终之前无法引用字段。研究 Java 的这一方面相当困难,因为它大部分发生在幕后,并且在某种程度上取决于您了解并坚持“快乐之路”。
  • @davidalayachew 我修改了我的答案。我觉得现在好多了。我将清理上面的 cmets,但如果仍然不清楚,请告诉我。
【解决方案2】:

这不是一个真正的答案,但只要用匿名类替换第一个 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();
    }
}

【讨论】:

  • 如果这就是您要使用的,则您不需要供应商。您可以为第一个使用匿名类。这只适用于剪刀石头布具有循环关系,但对于复杂的状态转换模型,我怀疑这种“打破”循环关系的方法要么极其乏味,要么在物理上是不可能的。
  • 感谢您的答复。这让我发笑,因为它确实区分了两者之间的区别。也许如果我们得到紧凑的方法,这可能会更简洁。
【解决方案3】:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.3

根据那里的规则,匿名类似乎被认为是可以接受的,因为它是一个“不同的类”。

示例中有几个 cmet 表示“好的 - 发生在不同的类中”

【讨论】:

  • 感谢您的答复。我有点明白了。我想下一个问题是,为什么不将 lambda 体视为不同的类? lambda 的目的不是提供一个作用域是它自己的函数,而是从它所在的作用域中借用一些字段/方法/等吗?也许我错了。
【解决方案4】:

我不是专家,所以我可能是错的,但这是我的理解。

非法前向引用意味着您试图在定义变量之前使用它。 这就像说"i = 10; int i;"

前两个实际上使用传递的变量。 RPS0 和 RPS1 将未知变量 SCISSORS 分配给需要 RPS0/RPS1 变量的字段。这应该是预期的结果。

那么对于匿名类来说,它必须做一些不同的事情。 Java 必须重新排序定义以首先定义 RPS2 实例,然后再实例化它们。

【讨论】:

  • 谢谢您的回答。我明白你在说什么。我知道使用枚举值的行为会一直保存到实际调用该方法为止。我的困惑是,为什么 lambda 不会发生同样的情况? lambda 不应该使用枚举值。即使像 ROCK(() -&gt; {return RPS1.SCISSORS;}), 这样的东西仍然失败。
猜你喜欢
  • 2013-01-28
  • 1970-01-01
  • 2021-12-23
  • 1970-01-01
  • 2020-05-24
  • 1970-01-01
  • 2016-09-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多