【问题标题】:Why can't I access static final members from a dedicated enum value in Java为什么我不能从 Java 中的专用枚举值访问静态最终成员
【发布时间】:2015-12-18 03:13:29
【问题描述】:

我想知道为什么,虽然在 Java 中执行以下操作是完全有效的

public enum Test {
   VALUE1() {
      public static final String CONST_RELATED_TO_VALUE1 = "constant";
      public static final String OTHER_CONST_RELATED_TO_VALUE1 = "constant";
   },
   VALUE2() {
      public static final String CONST_RELATED_TO_VALUE2 = "constant";
   },
   VALUE3;
}

使用Test.VALUE1.CONST_RELATED_TO_VALUE1 访问常量不起作用。

现在我明白了,VALUE1VALUE2 等实际上通常都被视为Test 类型的静态最终实例,因此没有这些字段,但理论上这些信息应该在编译时可用,这可以通过little test 轻松验证

     // print types and static members
     for (Object o: Test.values()) {
        System.out.println(o.toString() + ": " + o.getClass());
        for (Field field : o.getClass().getDeclaredFields()) {
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
                System.out.println("\t" + field);
            }
        }             
     }

导致以下输出

VALUE1: class Test$1                                                                                                                                                                               
        public static final java.lang.String Test$1.CONST_RELATED_TO_VALUE1                                                                                                                
        public static final java.lang.String Test$1.OTHER_CONST_RELATED_TO_VALUE1                                                                                                          
VALUE2: class Test$2                                                                                                                                                                               
        public static final java.lang.String Test$2.CONST_RELATED_TO_VALUE2                                                                                                                
VALUE3: class Test                                                                                                                                                                                 
        public static final Test Test.VALUE1                                                                                                                                    
        public static final Test Test.VALUE2                                                                                                                                    
        public static final Test Test.VALUE3                                                                                                                                    
        private static final Test[] Test.$VALUES

很明显,我们实际上在运行时为VALUE1VALUE2 提供了适当的专用子类。但看起来我们丢失有关VALUE1VALUE2 的具体类型信息的原因也是编译器为VALUE3 使用的基枚举类Test 生成静态枚举值的方式:所有成员都属于输入Test 并丢弃具体类型。

但是,在我看来,如果编译器只是像这样保留这些类型

        public static final Test$1 Test.VALUE1                                                                                                                                    
        public static final Test$2 Test.VALUE2                                                                                                                                    
        public static final Test Test.VALUE3                                                                                                                                    

所有周围的代码仍然可以工作。此外,我们还可以执行我最初尝试的操作并通过Test.VALUE1 访问CONST_RELATED_TO_VALUE1,这显然是Test$1 类型的实例,而不仅仅是Test,虽然通常应该避免它,但似乎在这种情况下通过实例访问该静态成员非常好。

现在正如许多人正确指出的那样,在左侧使用匿名类不是有效的 Java 代码,而且如果没有一些重大的规范更改,编译器也可能不允许使用。但是,这可以通过使用命名内部类轻松解决,因此我们将拥有

        public static final Test.Value1 Test.VALUE1                                                                                                                                    
        public static final Test.Value2 Test.VALUE2                                                                                                                                    
        public static final Test Test.VALUE3                                                                                                                                    

这甚至为调试提供了一个额外的好处,即内部子类名称清楚地映射到相应的枚举值。

现在我知道必须进行一些小的更改,但是从匿名类到命名类并且不丢弃类型似乎是一个很小的更改,这看起来是一个相当不错的功能,没有简单的方法来模拟它使用覆盖的成员或其他东西。

所以我想知道为什么除了时间之外没有这样实现?我是否在这里遗漏了一些重要的东西,会阻止编译器执行此操作(无论是关于实现复杂性还是类型系统的不可能性),是否只是没有实现它以使其更简单,因为没有时间或类似的东西?

(我主要是在寻找为什么决定从编译器/类型系统的角度来实现它的原因,而不是实际的替代方案,因为肯定有几个,尽管它看起来仍然不错模式)

【问题讨论】:

  • 您可能想阅读this
  • @SotiriosDelimanolis:是的,我明白你的意思,所以我们通常不能有匿名类的成员作为程序员,我猜这扩展到编译器,不能有任何匿名类字段等。(这是本质上也是@Pshemo 写的)但是如果那是它崩溃的地方,请将其设为命名类Test.Value1,它应该可以再次工作。

标签: java types enums compiler-construction


【解决方案1】:

CONST_RELATED_TO_VALUE1 等静态成员是对应枚举值的匿名类的成员,但不是枚举类本身的成员。与其他匿名类一样,这里的对象VALUE1 被声明为Test 类型,即使它是Test 的匿名子类的一个实例。

因此,您不能通过VALUE1.CONST_RELATED_TO_VALUE1 访问CONST_RELATED_TO_VALUE1,因为VALUE1Test 类型的引用,而CONST_RELATED_TO_VALUE1 是匿名子类的成员,但不是Test 的成员。 CONST_RELATED_TO_VALUE1 只能被匿名类的其他成员访问。

如果您想访问在VALUE1 的匿名类中定义的值,您需要有一个枚举类型的方法(例如,m()),该方法可以在枚举对象的匿名类中覆盖,并且返回或以某种方式提供您想要公开的值(通过VALUE1.m())。

【讨论】:

  • 是的,这都是真的,但仍然不能回答我的问题为什么编译器看到VALUE1 类型为Test 而不是类型Test$1它清楚地知道它实际上是(参见我链接的代码片段的输出)。
  • 不幸的是,对于编译器来说,匿名类没有被命名——这就是它们被称为匿名的原因。它们仅用于自定义行为而不添加其他静态类型。确实,他们最终使用带有后缀$1$2 等的约定获得了一个名称,但仅限于 JVM 使用。要点是引用的类型总是在“左侧”定义:Test VALUE1 = ...。在这种情况下,右侧的类型(匿名类)是 Test 的子类型,但不是引用 VALUE1 的类型。
  • 另外,关于您对m() 的建议:重点是我希望每个枚举值有不同的字段,因此在基类中添加方法将完全解决这个问题。正如我所写,除了简单/严格(即在编译时我们应该只看到Test 而不是具体类型)之外,我没有看到任何技术/类型理论限制的原因。
  • 是的,匿名类没有命名,但这里我们显然有编译时间常量Test.VALUE1,它总是有一个Test$1类型的实例。编译器确实知道这一切。现在我认为它实际上不起作用的原因是它实现成员的方式,因为它们始终是 Test 类型而不是它们的具体类型(它们可能是)。
  • 在某种意义上是令人沮丧的。除了使用像m() 这样的通用方法来公开特定于实例的值之外,使用反射是我能快速想到解决这个问题的唯一方法。也许一个好的库或其他一些 JVM 语言可以使访问这些字段更容易。
【解决方案2】:

枚举常量是特殊变量,属于它们声明的枚举类型。换言之,引用表达式Test.VALUE1 的类型为TestTEST 类型没有定义变量名称 CONST_RELATED_TO_VALUE1,因此您无法访问。

这类似于做

class Parent {
}

class Child extends Parent {
    public Object field = new Object();
}
...
Parent ref = new Child(); 
System.out.println(ref.field); // compilation error

除非您尝试通过引用表达式访问 static 字段。


枚举常量的可选体定义了扩展枚举类型的新匿名类。

【讨论】:

  • 根据我之前的评论,我想我已经在我的问题中写了所有这些,可能还不够清楚:我仍然不清楚为什么编译器不利用它的知识实际上在编译时知道,在您的示例中,我们总是有一个 Child 的实例。
  • @JanickBernet 在我给出的Child 示例中更为明显。编译器在运行时不知道ref 变量将包含什么。无论是对Child 对象的引用,还是对SomeOtherSubclass 的引用。枚举类型只是特殊的引用类型。编译器对它们的方法或字段访问表达式没有不同的规则。
【解决方案3】:
VALUE1() {
  public static final String CONST_RELATED_TO_VALUE1 = "constant";
  public static final String OTHER_CONST_RELATED_TO_VALUE1 = "constant";
}

是一个扩展Test枚举的匿名类。对于这样的类,我们只能在创建后直接访问它的成员(无需反射),例如:

class Foo{
    public static void main(String[] args) {
        System.out.println(new Foo(){
            public String x = "foo";
        }.x);
    }
}

但是如果我们这样写:

Foo f = new Foo(){
    public String x = "foo";
};
System.out.println(f.x);

我们将收到编译错误,因为 f 的类型为 Foo,它没有声明 x 成员。
这就是您的枚举的问题。你在这里做了什么:

VALUE1() {
  public static final String CONST_RELATED_TO_VALUE1 = "constant";
  public static final String OTHER_CONST_RELATED_TO_VALUE1 = "constant";
}

其实是:

public static final Test VALUE1 = new Test(){
//                  ^^^^^^^^^^^
  public static final String CONST_RELATED_TO_VALUE1 = "constant";
  public static final String OTHER_CONST_RELATED_TO_VALUE1 = "constant";
}

如您所见,VALUE1(和其他枚举常量)的类型为 Test,而不是 Test$1(编译器给出的匿名类的名称)。

为什么选择Test 类型而不是Test$1?嗯,可能是因为变量不能用匿名类型声明(我们不能有Test$1 foo变量)而且所有的枚举类型实际上都编译成简单的类它扩展了 Enum 类,因此必须对其字段(常量)应用相同的规则。

【讨论】:

  • 这不是问题,我总是可以通过对象实例访问静态成员。关于接口,正如我所说,我很清楚这就是它当前实现的方式,但我看不出有任何理由必须这样,编译器知道那个时候的确切类型。
  • @JanickBernet 我重写了我的答案。
  • 谢谢,这更符合我们的要求。也就是说,我不确定“变量不能用匿名类型声明”:我很确定编译器没有这个限制,但它可能涉及更多。如果编译器的工作方式将枚举转换为您编写的代码,然后在单独的步骤中生成匿名类,那么这样做是有道理的。但我猜它确实会一次完成所有枚举创建。你知道这是事实吗?绝对是迄今为止最好的解释。
  • 编译器仅限于 Java 语言规范,我不记得 JLS 允许我们创建此类变量,这也可能阻止编译器创建它们。现在这是一个几乎睡着了的人的疯狂猜测,所以我可能是错的,但这是我今天的最后一个 Java(可能)。
  • 实际上,如果问题是匿名类,只需创建命名内部类Test$Value1Test$Value2 就可以解决这个问题,而且实际上更清楚地映射到相应的值。跨度>
猜你喜欢
  • 2011-01-18
  • 2018-11-05
  • 1970-01-01
  • 1970-01-01
  • 2010-10-01
  • 2021-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多