5

I am trying to represent a State Transition Diagram, and I want to do this with a Java enum. I am well aware that there are many other ways to accomplish this with Map<K, V> or maybe with a static initialization block in my enum. However, I am trying to understand why the following occurs.

Here is a(n extremely) simplified example of what I am trying to do.


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

Obviously, this fails due to an illegal forward reference.

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

That's fine, I accept that. Trying to manually insert SCISSORS there would require Java to try and setup SCISSORS, which would then trigger setting up PAPER, which would then trigger setting up ROCK, leading to an infinite loop. I can easily understand then why this direct reference is not acceptable, and is prohibited with a compiler error.

So, I experimented and tried to do the same with 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();
    }
}

It failed with basically the same error.

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

I was a little more bothered by this, since I really felt like the lambda should have allowed it to not fail. But admittedly, I didn't understand nearly enough about the rules, scoping, and boundaries of lambdas to have a firmer opinion.

By the way, I experimented with adding curly braces and a return to the lambda, but that didn't help either.

So, I tried with an anonymous class.

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

Shockingly enough, it worked.

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

So then, I thought to search the Java Language Specification for Java 19 for answers, but my searches ended up returning nothing. I tried doing Ctrl+F searches (case-insensitive) for relevant phrases like "Illegal", "Forward", "Reference", "Enum", "Lambda", "Anonymous" and more. Here are some of the links I searched. Maybe I missed something in them that answers my question?

None of them answered my question. Could someone help me understand the rules in play that prevented me from using lambdas but allowed anonymous classes?

EDIT - @DidierL pointed out a link to another StackOverflow post that deals with something similar. I think the answer given to that question is the same answer to mine. In short, an anonymous class has its own "context", while a lambda does not. Therefore, when the lambda attempts to fetch declarations of variables/methods/etc., it would be the same as if you did it inline, like my RPS0 example above.

It's frustrating, but I think that, as well as @Michael's answer have both answered my question to completion.

EDIT 2 - Adding this snippet for my discussion with @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();
      
      }
   
davidalayachew
  • 1,279
  • 1
  • 11
  • 22
  • Interesting experiment. https://jenkov.com/tutorials/java/lambda-expressions.html states "Java lambda expressions can only be used where the type they are matched against is a single method interface". So it looks like where you tried to apply a lambda is not an ok place to apply it. – Zack Macomber Jan 10 '23 at 16:31
  • 1
    @ZackMacomber Thank you for your response. I am not sure you are correct though. Isn't the interface I am matching against supposed to be my `Supplier`? – davidalayachew Jan 10 '23 at 16:32
  • 1
    Very good question but I've edited it for brevity. I don't think your (unfortunately fruitless) searches really add much, and I think it's a better question without them. If you strongly disagree then feel free to add it back, but perhaps edit to the salient points. – Michael Jan 10 '23 at 16:33
  • @Michael I see your edits. Thank you for the changes. I made a simple bullet list of the searches I tried to make. That should satisfy brevity while allowing people's support to be more informed/directed. Please edit my edit if you feel it should be different. – davidalayachew Jan 10 '23 at 16:43
  • 2
    Does this answer your question? [Accessing Same Class' enum value inside an enum declaration within a lambda doesn't compile](https://stackoverflow.com/questions/55162147/acessing-same-class-enum-inside-an-enum-constructor-within-a-lambda-doesnt-com) – Didier L Jan 10 '23 at 17:00
  • @DidierL Ohhhh, I think you found it Didier. I am reading further, but this might be it – davidalayachew Jan 10 '23 at 17:02
  • @DidierL I think that you are correct. Both of our questions fail for the reason mentioned in the answer. I have edited my answer to include a reference to that question and a simple explanation. Thank you for the help. https://stackoverflow.com/a/55166692/10118965 – davidalayachew Jan 10 '23 at 17:21

4 Answers4

3

I believe this is rooted in what the JLS calls "definite assignment".

First, it may be helpful to outline the order in which the enum is initialized for your first example.

  1. Some other class refers to the enum class for the first time, e.g. RPS0.ROCK.winsAgainst()
  2. The "clinit" (static initializer is invoked)
  3. The enum constants are initialized in order of declaration
    1. An equivalent of public static final RPS0 ROCK = new RPS0(SCISSORS);
    2. An equivalent of public static final RPS0 PAPER = new RPS0(ROCK);
    3. An equivalent of public static final RPS0 SCISSORS = new RPS0(PAPER);
  4. The enum class is now loaded
  5. The expression RPS0.ROCK is evaluated for that referencing class in #1
  6. winsAgainst is invoked on that instance.

The reason this fails on the ROCK line is because SCISSORS is not "definitely assigned". Knowing the order of initialization, we can see it's even worse than that. Not only is it not definitely assigned, it's definitely not assigned. The assignment of SCISSORS (3.3) happens after ROCK (3.1).

SCISSORS would be null when ROCK tried to access it, if the compiler were to allow this. It's assigned later.

We can see this for ourselves if we introduce some indirection. The definite assignment problem is now gone because our constructors aren't referencing the fields directly. The compiler isn't checking for any definite assignment in the constructor expression. The constructor is using result of a method invocation, not a field.

All we've done is trick the compiler into allowing something that's going to fail.

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

The lambda case is pretty much identical. The value is still not definitely assigned. Consider a case where the enum constructor calls get on the Supplier. Refer back to the order of initialization above. In the example below, ROCK would be trying to access SCISSORS before it was initialized, and that's a potential bug that the compiler is trying to protect you from.

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

Annoying, really, because you know you're not using the Supplier in that way, and that's the only time it may not be assigned yet.

The reason the abstract class works is because the expression-target in the constructor is now gone completely. Again, refer back to the order of initialization. You should be able to see that for anything which calls winsAgainst, e.g. the example in #1, that invocation (#6) necessarily occurs after all enum constants have already been initialized (#3). The compiler can guarantee this access is safe.

Putting together two of the things we know - that we can use indirection to stop the compiler complaining about lack of definite assignment, and that Supplier can supply a value lazily - we can create an alternative solution:

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

This is provably safe, provided that the constructor never calls Supplier.get (including any methods the constructor itself calls).

Michael
  • 41,989
  • 11
  • 82
  • 128
  • Thank you for your response. I don't know if binding is the right word, but it definitely points me in the right direction. To make sure I understand, it sounds like you are saying that the real issue here is that Java attempts to "Bind" earlier for lambdas than it does for anonymous classes, thus causing this issue to occur? – davidalayachew Jan 10 '23 at 16:54
  • Expression target is a very helpful phrase, thank you. And I understand you better now. It's still very off-putting to me though that the lambda solution fails, but yours succeeds. It is almost as if they put in a check to prevent what we are trying to do, but your solution just happens to be one level of indirection further than they cared to prevent, while mine was not. For right now, I'll accept this as the answer, but this is aggravating to be frank. – davidalayachew Jan 10 '23 at 17:09
  • Also, someone in the comments recommended this link -- https://stackoverflow.com/questions/55162147/acessing-same-class-enum-inside-an-enum-constructor-within-a-lambda-doesnt-com -- I bring it up in case you can make more sense of it than I can. I am still struggling to grasp it, but it sounds like your answer and theirs is implying the same thing. – davidalayachew Jan 10 '23 at 17:11
  • 2
    "It's really nothing directly to do with lambdas or anonymous classes" - I think it does directly have to do with "classes". The instant you use a DIFFERENT (whether anonymous or whatever) class, the illegal forward reference goes away. Lambda expressions are not classes as far as I know. – Zack Macomber Jan 10 '23 at 17:18
  • @ZackMacomber I understand what you are saying. Another answer to a different question brought up the concept of a "context", saying that the JLS says the context for a lambda is different than that of an anonymous class. Here is the answer -- https://stackoverflow.com/a/55166692/10118965 – davidalayachew Jan 10 '23 at 17:25
  • @Michael I see your edit. I don't understand about the null. Could you explain that further? – davidalayachew Jan 10 '23 at 17:26
  • 1
    @Michael Silly me, I should have run it before I commented. I see what you mean now. Thank you. – davidalayachew Jan 10 '23 at 17:29
  • @Michael And I see your latest edit. That clarifies things a lot more, thank you. So, in short, lambdas, methods, and any other form of indirection simply isn't going to work because they all pass in the field prior to its initialization. You demonstrated how we can trick the compiler into not catching the danger in us doing that, but it doesn't take away from the fact that all we are doing is stopping the compiler from saving us from doing something bad. In reality, the only way to achieve what I want is through the anonymous class, because it doesn't evaluate anything until method call. Yes? – davidalayachew Jan 10 '23 at 17:35
  • 1
    @Michael Thank you for your patience in helping me understand this. Scoping and how variables are picked up is one of the things I never really understood about Java that well. It took me a long time to understand about lambdas and how they cannot reference fields until they are effectively final. It's rather difficult to research this facet of Java, since most of it happens under the hood, and kind of depends on you knowing and staying on the "Happy Path". – davidalayachew Jan 10 '23 at 17:46
  • @Michael It is an excellent answer, thank you for doing this. This clarifies a lot. Funnily enough, your question led me to another -- the part where you said "Annoying, really", aren't we just as vulnerable to that via Anonymous classes as we would be with the `Supplier`? I edited my question, please see what I mean. It is `RPS4`. – davidalayachew Jan 10 '23 at 19:08
  • 2
    @davidalayachew Yeah, RPS4 still fails. Change the field initializer to `this.winsAgainst = Objects.requireNonNull(this.winsAgainst());` It will blow up. It's worth noting that the field is not adding any value here, though. You can just call `winsAgainst()` every time you would access the field, and then it will work. – Michael Jan 10 '23 at 20:02
  • @Michael Oh absolutely, I am being intentionally obtuse to show that the warning is a rather short fence. Regardless, with this edit, I think you struck the issue at the heart -- the JDK folks gave a stronger set of compiler errors to lambdas than they did for anonymous classes, with the intent of protecting us, referencing definite assignment and the JLS links I posted. That said, both solutions are still prone to indirection and damage. And because of all of this, going the way that you did, with a `Supplier` (or my anonymous class) is the "safest" solution still allowed. Thank you so much! – davidalayachew Jan 10 '23 at 20:09
  • And to be clear, these "protections" are them making a best effort attempt at protecting people from stumbling into the same pothole. But of course, they can't (or chose not to) make the check 100% exhaustive for every possibility. And as a result, this is still possible to break, like you showed with null. That said, they made a greater effort attempt to protect us with lambdas, which explains the discrepancies. Not to mention, lambdas have more room for abuse, and the problems with anonymous classes had been well known at that point. If all of this is true, then I understand entirely now. – davidalayachew Jan 10 '23 at 20:12
  • 1
    Thank you again for taking the time to walk me through this. Thanks to yours and everyone else's efforts, I feel like I understand the full scale of the "why", and not just the "what". – davidalayachew Jan 10 '23 at 20:13
1

Not really an answer, but as soon as you replace the first lambda expression with an anonymous class, it gets to work.

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();
    }
}
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
  • 1
    If that's what you're going with, you don't need the supplier. You can use an anonymous class for the first one. This only works because rock papers scissors has a cyclic relationship, but for a complex state transition model, I suspect this method of "breaking" the cyclic relationships is either extremely tedious, to physically impossible. – Michael Jan 10 '23 at 16:53
  • 1
    Thank you for your response. This makes me laugh because it really ties down the difference between the 2. Maybe if we get compact methods, this might be more terse. – davidalayachew Jan 10 '23 at 16:57
1

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

Based on the rules in there, it looks like the anonymous class is considered acceptable because it's a "different class".

There are several comments in the example that say "ok - occurs in a different class"

Zack Macomber
  • 6,682
  • 14
  • 57
  • 104
  • Thank you for your response. I sort of understand. I guess the next question is, why isn't a lambda body considered a different class? Isn't the purpose of a lambda to provide a function whose scope is it's own, but with some fields/methods/etc borrowed from the scope that it is in? Perhaps I am wrong. – davidalayachew Jan 10 '23 at 17:05
0

I'm not an expert so I could be wrong but this is my understanding.

An Illegal Forward Reference means that you're trying to use a variable before it has been defined. It's like saying "i = 10; int i;".

The first two actually use the variable that was passed. RPS0 and RPS1 assign an unknown variable SCISSORS to a field expecting an RPS0/RPS1 variable. This should be the expected result.

For the Anonymous Class then, it must be doing something different. Java must be re-ordering the definitions to first define the RPS2 instances first, then instancing them after.

CSDragon
  • 129
  • 1
  • 10
  • Thank you for your answer. I see what you are saying. I understand that the act of using the enum value is saved until the method is actually called. My confusion is, why doesn't the same occur for the lambda? The lambda should not be using the enum value yet. Even something like `ROCK(() -> {return RPS1.SCISSORS;}),` still fails. – davidalayachew Jan 10 '23 at 16:45