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();
}
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.
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
is evaluated for that referencing class in #1winsAgainst
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).