Enum of Enum is NULL

2019-02-16 13:12发布

问题:

I'm developing a LALG compiler to my college course on Java 1.6. So I did a types class and grammar class.

EnumTypes

public enum EnumTypes {

    A("OLA"),
    B("MUNDO"),
    C("HELLO"),
    D("WORLD"),

    /**
     * The order below is reversed on purpose.
     * Revert it and will you get a NULL list of types furder.
     */

    I(EnumGrammar.THREE),
    H(EnumGrammar.TWO),
    F(EnumGrammar.ONE),
    E(EnumGrammar.ZERO);

    private String strValue;
    private EnumGrammar enumGrammarValue;

    private EnumTypes(String strValue) {
        this.strValue = strValue;
    }

    private EnumTypes(EnumGrammar enumGrammarValue) {
        this.enumGrammarValue = enumGrammarValue;
    }

    public String getStrValue() {
        return strValue;
    }

    public EnumGrammar getEnumTiposValue() {
        return enumGrammarValue;
    }
}

EnumGrammar

public enum EnumGrammar {

    ZERO(EnumTypes.A,EnumTypes.B,EnumTypes.F,EnumTypes.D),
    ONE(EnumTypes.C),
    TWO(EnumTypes.B,EnumTypes.H),
    THREE(EnumTypes.D,EnumTypes.A,EnumTypes.C);

    private EnumTypes[] values;

    private EnumGrammar(EnumTypes ... values) {
        this.values = values;
    }

    public EnumTypes[] getValues() {
        return values;
    }
}

When I call EnumTypes.E.getEnumTiposValue().getValues() where are supposed to be the EnumTypes.F value is NULL.

Main

public class Main {

    public static void main(String[] args) {
        //prints [A, B, null, D]
        System.out.println(Arrays.toString(EnumTypes.E.getEnumTiposValue().getValues()));
    }

}

There are a workaround or something like that?

Thanks!

回答1:

Essentially, it is always a very risky thing to allow a reference to an object to get outside of the class before the class is fully constructed, that is before the constructor is finished. Enums are singletons. Here you have two classes whose constructors receive each other's instances in a circular dependency. Add to this that class loading is lazy, so the classes will be loaded and enum instances created as you go and it sounds quite reasonable that the ends result depends on the order in which the enums are initialized.

I can't quote the corresponding point from JLS right now (I'll look for it), but I believe that if you allow a reference to an object to "leave the class" from outside of the constructor (which happens here due to enums being singletons initialized by the JVM), the JVM is free to do something strange.

EDIT: these points from the JLS are of importance for the case:

  • 17.5.2 - A read of a final field of an object within the thread that constructs that object is ordered with respect to the initialization of that field within the constructor by the usual happens-before rules. If the read occurs after the field is set in the constructor, it sees the value the final field is assigned, otherwise it sees the default value. Since enum values are internally treated like static final fields (see 16.5 below), if you reference one enum from inside the constructor of another enum whose constructor references the first one, at least one of these two objects will not yet have been fully initialized and so the reference may still be null at this point.
  • 16.5 - The definite assignment/unassignment status of any construct within the class body of an enum constant is governed by the usual rules for classes
  • 8.3.2 - rules for initialization of fields
  • 12.4.1 - when initialization occurs


回答2:

Here is what's happening, in order:

  1. Your code calls EnumTypes.E.getEnumTiposValue(), triggering class loading of EnumTypes.
  2. Static initialization of EnumTypes begins - its enum constants will be initialized in the order they're declared.
  3. EnumTypes.A through EnumTypes.D are initialized.
  4. EnumTypes.I begins initialization - its constructor call references EnumGrammar.THREE, triggering class loading of EnumGrammar.
  5. Static initialization of EnumGrammar begins - its enum constants will be initialized in the order they're declared.
  6. EnumGrammar.ZERO is initialized - its constructor call references EnumTypes.A, EnumTypes.B, EnumTypes.F, and EnumTypes.D. Out of those, EnumTypes.F has not yet been initialized. Therefore, the reference to it is null.

From there, static initialization of the two enum classes finishes, but it doesn't matter for EnumGrammar.ZERO - its values field has already been set.



回答3:

For the workaround, suppose that you have EnumA and EnumB, I will just put EnumB's name in EnumA's constructor.

When you have to retrieve EnumB from EnumA, you can simply use EnumB.valueOf(EnumA.this.enumB)

For example, Question is the EnumB

public enum Question {
RICH_ENOUGH(R.string.question_rich_enough, Arrays.asList(Answer.RICH_ENOUGH_YES, Answer.RICH_ENOUGH_NO)),
ARE_YOU_SURE(R.string.question_are_you_sure, Arrays.asList(Answer.ARE_YOU_SURE_YES, Answer.ARE_YOU_SURE_NO)),
FOUND_A_NEW_JOB(R.string.question_found_new_job, Arrays.asList(Answer.FOUND_A_NEW_JOB_YES, Answer.FOUND_A_NEW_JOB_NO)),
// ...

and Answer is the EnumA

public enum Answer {
    RICH_ENOUGH_YES(R.string.answer_yes, "ARE_YOU_SURE"),
    RICH_ENOUGH_NO(R.string.answer_no, "THAT_SOMEBODY"),
    ARE_YOU_SURE_YES(R.string.answer_yes, null),
    ARE_YOU_SURE_NO(R.string.answer_no, "FOUND_A_NEW_JOB"),
    FOUND_A_NEW_JOB_YES(R.string.answer_yes, "GO_FOR_NEW_JOB"),
    // ...

    private final int answerStringRes;
    // Circular reference makes nulls
    private final String nextQuestionName;

    Answer(@StringRes int answerStringRes, String nexQuestionName) {
        this.answerStringRes = answerStringRes;
        this.nextQuestionName = nexQuestionName;
    }

Whenever I need to get the next Question from an Answer

public Question getNextQuestion() {
    if (nextQuestionName == null) {
        return null;
    }
    return Question.valueOf(nextQuestionName);
}

This should be simple enough to be a workaround.

Example source: an open source Android app for fun I just written last night - Should I Resign?