Java:枚举常量中方法和变量的定义

11

我正在做一些实验,无意中写了一段代码,非常奇怪,我完全不理解它。我甚至很惊讶自己居然能编译通过。代码如下:

enum Foo {
    VALUE_1 {
        public int myVariable = 1;
    },
    VALUE_2 {
        public void myMethod() {
            //
        }
    },
    VALUE_3;
}

正如预料的那样,无法通过以下方式访问该元素:

Foo.VALUE_2.myMethod();

原因是编译器会在枚举本身中查找该方法。

我认为不能从枚举外部访问这些方法和变量。因此,我尝试创建一个带参数的构造函数,并使用一些内部变量调用它:

enum Foo {
    VALUE(internalVariable) {
        int internalVariable = 1;
    };

    private Foo(int param) {
        //
    }
}

无法编译此构造。现在我在想,如果没有访问它的方式,为什么要在常量内定义某些内容。

我试图在常量中以及枚举本身中创建同名方法,以查看是否会发生冲突。但并未发现冲突!

enum Foo {
    VALUE_1 {
        int myVariable = 1;

        public int myMethod() {
            return myVariable;
        }
    },
    VALUE_2 {
        //
    };

    public int myMethod() {
        return 0;
    }
}

接下来是有趣的时刻!我试图在枚举内调用myMethod(),并实际上弄清了这种Java魔法的工作原理。在常量内定义的方法会覆盖在枚举内定义的方法。

Foo.VALUE_1.myMethod(); // Returns 1
Foo.VALUE_2.myMethod(); // Returns 0

然而,我们无法覆盖变量,对吗?因此,我很好奇,仅使用变量该怎么办。
enum Foo {
    VALUE_1 {
        public int myVariable = 1;
    },
    VALUE_2 {
        //
    };

    public int myVariable = 0;
}

....

System.out.println(Foo.VALUE_1.myVariable); // Returns 0
System.out.println(Foo.VALUE_2.myVariable); // Returns 0

现在我终于要开始问问题了:
  1. Why I don't get any error if I create public method inside the constant and left enumeration empty without this method? In that case, the method I just defined can't be called at all. Or am I wrong?

    Update: I know that enumeration can implement interface. However, if I haven't specifically said that, whole code is pointless.

    Someone pointed out that even if method can't be accessed from the language in the normal way, it's still possible to use reflection. Well... Why don't we design an inaccessible keyword?

    inaccessible void magicalMethod() {
         //
    }
    

    Such a method will be compiled into the *.class file. When you want to use it, you've to load bytecode by yourself and interpret it.

    I just can't understand, why it's possible to define unreachable method. The only reason I can think is that programmer is working and doesn't have definition of interface yet. So he's just preparing code of single methods and will add "implements" keyword later. Beside this is illogical, it would still require to have such a method in all constants.

    I think this should end up with error, not just warning about unused method. You may forget to add "implement" clause or to define method in the enumeration (which would be overridden) and will realize that just after the first use. Java is very strict language, so I'd expect this behavior.

  2. Why I don't get any error if I create public variable (or field, to be more precise) inside the constant? It can't be accessed in the any case (from the outside). Therefore, modifier "public" doesn't make any sense here.

    Update: It's more less the same thing as in the previous point, except the visibility modifier is completely useless here. It really doesn't matter if it's public, protected or private, because you won't be able to access that anyway. I think this is a bug.

  3. Why it's possible to define a class (without visibility modifiers), but not interface? Yeah, you wouldn't probably want to write so brutal enumeration that you would need to define classes inside the constant and even to use inheritance there. But if it's possible to define classes and abstract classes, it seems little weird.

    Update: This is definitely not something you'd need on regular basis, but I understand that it might be useful. But why it's limited to classes only and interfaces can't be defined as well?

    enum Foo {
        VALUE {
            class MyClass {
                // OK
            }
    
            abstract class MyAbstractClass {
                // OK
            }
    
            interface MyInterface {
                // FAIL. It won't compile.
            }
        }
    }
    
  4. Did you use such a functionality somewhere? I can imagine it might be useful, but it's little confusing. Also, when I was searching for some resources about that, I didn't find anything.

    Update: I'd like to see some practical example wth overridden methods in an enum constant class body. Have you seen it in some open-source project?

环境:

$ java -version
java version "1.7.0_21"
OpenJDK Runtime Environment (IcedTea 2.3.9) (7u21-2.3.9-0ubuntu0.12.10.1)
OpenJDK 64-Bit Server VM (build 23.7-b01, mixed mode)

感谢您的时间和回答!

您可以在Maker Factories开源项目中的枚举常量中找到重写方法的实际示例:http://www.github.com/raspacorp/maker - raspacorp
4个回答

4

好的,实际上我已经使用了这个功能!我正在编写一个简单的游戏,并希望提供两个声音包。因为游戏非常简单,未来可能不会进行扩展,所以我不想创建一些复杂的机制来实现这样的事情。

public enum SoundPack {
    CLASSICAL {
        @Override
        public String getSoundPickUp() {
            return "res/sounds/classical/pick.wav";
        }

        @Override
        public String getSoundNewLevel() {
            return "res/sounds/classical/success.wav";
        }

        @Override
        public String getSoundFail() {
            return "res/sounds/fail.wav";
        }
    },
    UNHEALTHY {
        @Override
        public String getSoundPickUp() {
            return "res/sounds/unhealthy/quick_fart.wav";
        }

        @Override
        public String getSoundNewLevel() {
            return "res/sounds/unhealthy/toilet_flush.wav";
        }

        @Override
        public String getSoundFail() {
            return "res/sounds/unhealthy/vomiting.wav";
        }
    };

    public abstract String getSoundPickUp();
    public abstract String getSoundNewLevel();
    public abstract String getSoundFail();
}

所以,我刚刚定义了你在上面看到的枚举。在包含所有配置的类中,只有一个属性如下:

private SoundPack soundPack = SoundPack.CLASSICAL;

现在,如果我需要播放一些声音,我可以非常简单地获取路径:
configuration.getSoundPack().getSoundNewLevel();

可以通过将另一个值分配给 soundPack 字段来轻松更改配置,甚至在运行时也能如此。如果声音尚未加载(因为我经常使用懒加载),更改会立即生效,而无需更改其他任何内容。

此外,如果我想添加新的声音包,只需在该枚举中定义新的常量即可。Eclipse 将显示警告,我只需按下 CTRL + 1 并生成这些方法即可。所以也很容易。

我知道这不是最好的做法。但它简单、快速,并且最重要的是:我想在实践中尝试使用它。 :-)


3

[...] 我刚刚定义的方法根本无法被调用。或者我错了?

你错了:Java enum可以实现接口,就像这样:

interface Bar {
    void myMethod();
}
enum Foo implements Bar {
    VALUE_1 {
        public void myMethod() {
            System.err.println("val1");
        }
    };
}

现在你能够在VALUE_1中访问myMethod。当然,你需要在其他值或者enum本身中实现这个方法。此外,你可以通过反射访问那些无法通过语言本身访问的方法。
就公共变量而言,看起来反射是唯一的途径。不过,并没有理由完全禁用它们(虽然很难想象有什么有用的应用场景)。

你曾经在哪里使用过这样的功能?

我曾经使用过一个实现了接口的enum,每个常量都以其特定的方式实现了接口的方法。

你引用的第一部分是指在 enum 的主体中声明的公共方法,其中 OP _left enumeration empty without this method_。换句话说,这个方法不在任何接口中,并且是匿名类本地的。 - Sotirios Delimanolis
@SotiriosDelimanolis 不,这并不在枚举体内:该方法在个别值上,而不是在类上。但是该接口确实适用于整个枚举。 - Sergey Kalinichenko
我认为OP指的是在枚举常量内部声明的方法,而不是在枚举本身或实现的某个接口中声明的方法。没有任何地方。在这种情况下,除非通过反射,否则无法访问它,它是匿名的。 - Sotirios Delimanolis

3
“为什么我在常量内创建公共方法并将枚举留空,而不使用该方法时没有任何错误?在这种情况下,我刚刚定义的方法根本无法被调用。或者我错了吗?”实际上,编译器应该能够看到该方法在枚举常量的类体外不可见,并在未使用时警告您-我确定Eclipse会这样做。正如dasblinkenlight指出的那样,这样的公共方法实际上可能是枚举实现的接口声明的方法的重写。”
“我就是不明白,为什么可以定义无法访问的方法。我唯一能想到的原因是程序员正在工作,还没有接口定义。所以他只是准备单个方法的代码,并稍后添加“implements”关键字。除此之外是不合逻辑的,它仍然需要在所有常量中具有这样的方法。”正如我已经指出的,这并不特别适用于枚举常量类。有许多范围-私有嵌套类、本地类、匿名类-其中成员为公共是毫无意义的。
这个问题的困难在于只有语言设计者才能真正回答它。我只能给出我的意见,那就是:为什么要把它当作错误呢?语言规范并不是免费的——JLS中的所有内容都必须经过艰苦的定义、实现和测试。真正的问题是,将其视为错误有何好处?事实上,虽然未使用的成员可能表明存在错误(因此会有警告),但它并没有造成任何伤害

如果我在常量内部创建公共变量(或字段,更准确地说),为什么我没有收到任何错误?无论如何,它都不能从外部访问。因此,在此处使用修饰符“public”毫无意义。

与上面相同——编译器或至少一些IDE会在变量未被使用时发出警告。这与在私有嵌套类中声明public变量,然后在任何地方都没有引用它是相同的。无论如何,禁止这种情况并不是JLS的优先事项,尽管反射之眼能够看到所有东西。

这和前面的那个点差不多,只是可见性修饰符在这里完全没有用。无论是public、protected还是private都无关紧要,因为你根本无法访问它。我认为这是一个bug。
在这里,你忘记了成员可能仍然会在枚举常量类体内使用 - 比如说一个帮助方法。只是在这种情况下,访问修饰符根本不重要,可以省略。
为什么可以定义一个类(没有可见性修饰符),但不能定义接口?是的,你可能不想写一个需要在常量内部定义类甚至使用继承的暴力枚举。但如果可以定义类和抽象类,这似乎有点奇怪。
这是一个好问题,我花了一些时间才明白你的意思。澄清一下,你是说在这种情况下只允许使用类:
VALUE_1 {
    class Bar { }
    interface Baz { }
},

为了阐明这一点,请尝试将类设置为static
VALUE_1 {
    static class Bar { }
    interface Baz { }
},

现在两者都不允许。为什么?因为在枚举常量体中不能声明任何静态内容,因为该体处于该常量的实例上下文中。这类似于在内部(非静态嵌套)类的作用域中:
class Outer {

    class Inner {
        // nothing static allowed here either!
    }
}

在这样的范围内,静态变量、方法、类和接口(嵌套时隐式为静态)都是被禁止的。

你在某个地方使用了这样的功能吗?我可以想象它可能很有用,但有点令人困惑。此外,当我搜索关于它的一些资源时,我没有找到任何东西。

不清楚您具体指的是哪种功能。请更新问题以指定您正在寻找的确切内容 - 枚举常量类体中重写的方法?字段?辅助方法?辅助类?请澄清。


谢谢你的回答!你说得对,它确实产生了警告。我想问为什么它没有产生错误,而只是警告 :-)。当你发布这个问题时,我正在编辑它。 - tzima
@TomášZíma 我已经更新了我的答案以回应你的更新。 - Paul Bellora
谢谢!我已经更新了我的上一个问题。我再次考虑了一下,你可能是对的,没有必要产生错误。但是例如在接口中只能定义公共方法。否则会出现错误(“删除无效修饰符”)。至少在字段的情况下,这就是我所期望的(只允许“private”)。 - tzima

2
JLS

枚举常量的可选类体隐含地定义了一个匿名类声明(§15.9.5),该声明扩展了直接封闭的枚举类型。

创建类似这样的东西。
VALUE_2 {
    public void myMethod() {
        //
    }
},

当枚举本身(或其超类型)中没有声明myMethod()时,这将创建一个作用域为抽象类的方法,即不能在该体外部调用。在此体内声明的字段也具有类似的行为。 public 标识符不会改变任何内容。
编辑:
对于第三个问题,因为您正在声明匿名类,所以其他组件将无法访问(实现)该接口。另一方面,类提供行为,因此可以在匿名类中使用。
请查看一些关于匿名类的限制
至于第四个问题,我从未在实践中见过这样的代码。如果您想要一些辅助方法来执行仅与特定枚举相关的某些特定行为,则可能会很奇怪,并且可能更适合于一个真正的类。
看看使用枚举实现的单例模式。你可能想要有许多单例,每个都有自己的实现。使用重写方法的匿名枚举类声明可能是实现这一点的一种方式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接