枚举常量特定的类体是静态的还是非静态的?

33

我有一个枚举类型的类:

public enum Operation {
    PLUS() {
        @Override
        double apply(double x, double y) {       
            // ERROR: Cannot make a static reference
            // to the non-static method printMe()...
            printMe(x);
            return x + y;
        }
    };

    private void printMe(double val) {
        System.out.println("val = " + val);
    }

    abstract double apply(double x, double y);
}

正如您在上面看到的,我定义了一个枚举类型enum,它有一个值为PLUS。它包含了一个常量特定的主体。在它的主体中,我试图调用printMe(val);,但是我得到了编译错误:

无法将静态引用转换为非静态方法printMe()。

为什么会出现这个错误?我的意思是我正在覆盖PLUS体中的一个抽象方法。为什么它在static范围内?如何摆脱它?

我知道在printMe(){...}上添加static关键字可以解决问题,但如果我想保持printMe()为非静态的,是否有另一种方法呢?


另一个问题,与上述问题非常相似,但这次的错误消息听起来相反,即PLUS(){...}具有非静态上下文:

public enum Operation {
    PLUS() {
        // ERROR: the field "name" can not be declared static
        // in a non-static inner type.
        protected static String name = "someone";

        @Override
        double apply(double x, double y) {
            return x + y;
        }
    };

    abstract double apply(double x, double y);
}

我试图声明一个 PLUS 特定的 static 变量,但最终出现了错误:

 

无法在非静态内部类型中声明字段“name”为静态。

如果 PLUS 是匿名类,为什么不能在其中定义静态常量?这两个错误信息听起来互相矛盾,因为第一个错误信息说 PLUS(){...} 具有 静态 上下文,而第二个错误消息说 PLUS(){...} 具有 非静态 上下文。我现在更加困惑了。


我的问题是为什么我会得到这个错误,有没有一种方法可以解决它但又保持printMe()非静态。 - user842225
printMe()函数的访问权限改为protected,并在apply()函数内部调用this.printMe(val) - Alex Salauyou
@SashaSalauyou,是的,将其更改为protected可以使其正常工作。但是,我的问题是为什么会出现这个错误,请解释一下为什么更改为protected可以使其正常工作?请给出一个我可以接受的答案。重点是要理解原因而不仅仅是让它工作。谢谢。 - user842225
1
printMe 方法在你的枚举中是私有的,每一个枚举都是枚举类的实现,因此它们无法访问其父类中的私有方法。令我困惑的是错误的本质。 - Edwin Dalorzo
@EdwinDalorzo 这一点并不奇怪,enum的匿名子类将无法看到其父类的字段。修复方法是将 private void printMe(double val) { 改为 protected - EpicPandaForce
@EpicPandaForce 当然可以,但是消息的措辞错误地暗示了其他意思。 - user207421
8个回答

31

我来翻译一下。这是一个奇怪的案例。

看起来问题是:

  • 在这种情况下,私有成员应该是可访问的 (6.6.1.):

    否则,如果成员或构造函数被声明为 private,则只有在出现在封闭该成员或构造函数的顶级类的主体内部时才允许访问。

  • 然而,私有成员不会被继承 (8.2):

    一个类中被声明为 private 的成员不会被该类的子类继承。

  • 因此,printMe 不是匿名子类的成员,编译器会在超类* Operation 中查找它 (15.12.1):

    如果存在一个封闭类型声明,其中该方法是成员,则让 T 成为最内层的这样的类型声明。要搜索的类或接口是 T。

    这个搜索策略被称为“组合规则”。它在查找嵌套类的超类层次结构中的方法之前,先查找封闭类及其超类层次结构中的方法。

  • 这就是奇怪的地方。由于 printMe 在一个也封闭 PLUS 的类中被找到,因此调用该方法的对象被确定为是 Operation 的封闭实例,而这个实例不存在 (15.12.4.1):

    否则,让 T 成为该方法是成员的封闭类型声明,并让 n 成为一个整数,使得 T 是包含方法调用的类的声明的第 n 个词法封闭类型声明。目标引用是 this 的第 n 个词法封闭实例。

    如果 this 的第 n 个词法封闭实例不存在,则是编译时错误。

简而言之,因为printMe只是Operation的成员(没有被继承),编译器被迫在不存在的外部实例上调用printMe
然而,这个方法仍然是可访问的,我们可以通过限定调用来找到它:
@Override
double apply(double x, double y) {
//  now the superclass is searched
//  but the target reference is definitely 'this'
//  vvvvvv
    super.printMe(x);
    return x + y;
}

这两个错误信息听起来互相矛盾[...]。是的,这是该语言令人困惑的一面。一方面,匿名类从不是静态的(15.9.5),另一方面,匿名类表达式可以出现在静态上下文中,因此没有封闭实例(8.1.3)。
匿名类始终是内部类,从不是静态的。
在静态上下文中声明的内部类I的实例没有词法封闭实例。
为了帮助理解如何工作,这里有一个格式化的示例:

斜体字 中的所有内容都属于静态上下文。从 粗体字 中派生的匿名类被认为是内部且非静态的(但没有包含实例Example)。

由于匿名类是非静态的,它不能声明静态的非常量成员,尽管它本身在静态上下文中声明。


* 除了稍微混淆一下问题,Operation 是一个枚举类型这个事实是完全无关紧要的 (8.9.1):

枚举常量的可选类体隐式定义了一个匿名类声明,该声明扩展了直接封闭的枚举类型。类体受匿名类的通常规则控制[...]。


这绝对是我迄今为止读过的最好的答案。非常有趣。 - Edwin Dalorzo

10

我不认为我知道错误的本质答案,但也许我可以对讨论做出一点贡献。

当Java编译器编译您的枚举代码时,它会生成一个合成类,大致如下:

class Operation {

    protected abstract void foo();
    private void bar(){ }

    public static final Operation ONE;

    static {
        ONE = new Operation() {
            @Override
            protected void foo(){
                bar(); 
            }
        };
    }
}

你可以通过在其中一个枚举类中运行 javap 命令来验证枚举代码大致如下。

上面的代码给我带来了与您的枚举类相同的错误:“error: non-static method bar() cannot be referenced from a static context”。

所以在这里,编译器认为您不能从定义匿名类的静态上下文中调用实例方法 bar()

对我来说毫无意义,它应该是可访问或被拒绝访问,但该错误似乎不准确。我仍然感到困惑,但这似乎就是实际发生的情况。

如果编译器说匿名类无法访问其父类上的私有变量 foo,那么这将更有意义,但编译器却触发了这个错误。


通过您的示例,当您执行System.out.println(Operation.ONE.getClass());时,结果为class Operation$1,就像您所期望的那样。但是,当您使用枚举版本执行相同的操作时,结果只是class Operation。您知道这是为什么吗? - Paul Boddington
我不知道你的意思。当我在枚举版本和我上面手动定义的版本中执行System.out.println(Operation.ONE.getClass())时,我得到了Operation$1作为类名。请注意,除非您实际上在枚举中有一个抽象方法,否则匿名类是未定义的。 - Edwin Dalorzo
我正在使用Java 8,但这并没有什么区别。这一点没有改变。为了论证的目的,我刚刚尝试了Java 7,结果是一样的。 - Edwin Dalorzo
抱歉,你说得完全正确。我真的很愚蠢。我只是在做 enum Operation { ONE } - Paul Boddington
1
在这种情况下,这是有意义的,因为不会定义任何匿名类。它可以重用枚举类本身,因为该类是具体的。但是当枚举类是抽象的时,就需要定义实现抽象方法的具体类。在这些情况下,我们会看到编译器生成的合成代码中定义了匿名类。 - Edwin Dalorzo
谢谢,你能帮我检查一下我在帖子中的更新吗?谢谢。 - user842225

6

根据您的更新,随后的问题很容易回答。匿名类不允许有静态成员。

至于您最初的问题,最清晰的理解方法是尝试使用 this.printMe();。然后错误消息就更容易理解,并且给出了真正的原因,即为什么printMe();无法工作:

'printMe(double)' has private access in 'Operation'

你不能使用 printMe 是因为它是 private 的,并且 this 引用的编译时类型是 Operation 的匿名扩展类,而不是 Operation 本身 (见 Edwin Dalorzo 的答案)。当你只写 printMe(); 时,会得到不同的错误消息,因为由于某种原因编译器甚至没有意识到你正在尝试在 this 上调用实例方法。它会给出一个错误消息,如果你根本没有尝试在任何实例上调用 printMe (即好像它是一个静态方法一样)。如果你明确地写出 Operation.printMe();,错误消息不会改变。

绕过这个问题有两种方法:使 printMe 成为 protected,或者写

((Operation) this).printMe();

5

printMe不应该被声明为private,因为您要使用PLUS派生一个新的匿名类。

protected void printMe(double val) {

关于错误的性质,enum/Enum有点像一个人工制品;目前它让我无法理解:内部类可能会访问私有内容...


我认为这已经被确定了。问题是错误的奇怪性质。为什么是静态上下文错误,而不是简单的方法不存在或不可访问的经典信息? - Edwin Dalorzo
抱歉,我已经将我的回复添加到答案中了。我猜一个私有的静态方法可以被调用,就像一个普通的受保护方法一样。而编译器给出了一个过于狭窄的错误。 - Joop Eggen

4
PLUS()是哪种类型的?这基本上是enum Operation类型。
如果您想将其与Java类进行比较,那么它基本上是同一类别内部的一个object
现在,enum Operation具有抽象方法apply,这意味着任何该类型(即操作)都应实现此方法。到目前为止还不错。
现在是出现错误的棘手部分。
正如您所看到的,PLUS()基本上是Operation类型。 Operation具有private方法printMe(),这意味着只有enum Operation本身可以看到它,而不包括子枚举(就像Java中的子类和超类一样)。此外,这个方法也不是static,这意味着除非您实例化该类,否则无法调用它。因此,您最终会有两个解决问题的选择:
  1. printMe()方法设置为static,就像编译器建议的那样
  2. 将该方法的访问级别更改为protected,以便sub-enum继承此方法。

3
在这种情况下,使其静态化的原因是自动生成的合成访问器功能。如果它是私有静态的话,您仍将收到以下编译器警告。 “Access to enclosing method printMe(double) from the type Operation is emulated by a synthetic accessor method。”
在这种情况下,唯一不起作用的是私有非静态方法。所有其他安全性措施都有效,例如私有静态,受保护的非静态等。正如其他人提到的那样,PLUS是Operation的一个实现,所以私有技术上不起作用,Java会自动使用自动生成的合成访问器功能为您修复它。

2
使printMe方法成为静态方法可以解决编译错误:
private static void printMe(long val) {
    System.out.println("val = " + val);
}

1
我为此苦苦思索了一段时间,但我认为最好的理解方法是看一个不涉及enum的类似情况:
public class Outer {

    protected void protectedMethod() {
    }

    private void privateMethod() {
    }

    public class Inner {
        public void method1() {
            protectedMethod();   // legal
            privateMethod();     // legal
        }
    }

    public static class Nested {
        public void method2() {
            protectedMethod();  // illegal
            privateMethod();    // illegal
        }
    }

    public static class Nested2 extends Outer {
        public void method3() { 
            protectedMethod();  // legal
            privateMethod();    // illegal
        }
    }    
}

Inner类的对象是内部类,每个这样的对象都包含对封闭的Outer类对象的隐藏引用。这就是为什么调用protectedMethodprivateMethod是合法的原因。它们在包含的Outer对象上被调用,即hiddenOuterObject.protectedMethod()hiddenOuterObject.privateMethod()

一个 Nested 类的对象是一个静态嵌套类;没有与之关联的 Outer 类的对象。这就是为什么调用 protectedMethodprivateMethod 是非法的——它们没有 Outer 对象来操作。错误信息是“无法从静态上下文引用非静态方法 <method-name>()”。(请注意,在这一点上,privateMethod 仍然可见。如果 method2 有一个不同的类型为 Outer 的对象 outer,它可以合法地调用 outer.privateMethod()。但在示例代码中,没有对象可以操作。)

Nested2 类的对象同样是一个静态嵌套类,但它有一个扭曲的地方,即它继承了 Outer。由于 Outer 的受保护成员将被继承,因此可以合法地调用 protectedMethod();它将在 Nested2 类的对象上运行。然而,私有方法 privateMethod() 不会被继承。所以编译器对待它与对待 Nested 相同,这将产生相同的错误。

enum案例与Nested2案例非常相似。每个带有主体的枚举常量都会创建一个新的匿名子类Operation,但实际上它是一个静态嵌套类(尽管匿名类通常是内部类)。PLUS对象没有对Operation类对象的隐藏引用。因此,可以引用并操作PLUS对象的公共和受保护成员,但不能继承Operation中的私有成员,并且无法访问它们,因为没有可用的隐藏对象进行操作。错误消息“Cannot make a static reference to the non-static method printMe()”与“non-static method cannot be referenced from a static context”几乎相同,只是单词顺序不同。(我并不是说所有语言规则都与Nested2案例完全相同;但在这种情况下,将它们视为几乎相同类型的构造确实有所帮助。)
对受保护和私有字段的引用也适用相同的规则。

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