考虑以下代码:
enum E {
A { public int get() { return i; } },
B { public int get() { return this.i; } },
C { public int get() { return super.i; } },
D { public int get() { return D.i; } };
private int i = 0;
E() { this.i = 1; }
public abstract int get();
}
我得到编译时错误的前两个枚举常量声明(A &B)但最后两个编译良好(C &D).错误是:
A行错误1:非静态变量i不能从静态上下文中引用
有私有访问
B行错误2:i在E
由于get
是一个实例方法,我不明白为什么我不能以我想要的方式访问实例变量i
。
注意:从i
的声明中删除private
关键字也使代码可编译,这我也不明白。
使用Oracle JDK 7u9.
编辑
正如在注释中指出的,这不是特定于枚举的,下面的代码会产生相同的行为:
class E {
static E a = new E() { public int get() { return i; } };
static E b = new E() { public int get() { return this.i; } };
static E c = new E() { public int get() { return super.i; } };
static E d = new E() { public int get() { return d.i; } };
private int i = 0;
}
观察到的行为是由Java语言规范强制要求的,特别是对封闭类型字段的隐式访问,以及私有成员不可继承的规则。
非限定字段访问
A { public int get() { return i; } }
规范要求:
枚举常量的可选类体隐式地定义了一个匿名类声明(第15.9.5节),该声明扩展了立即封闭的枚举类型。类主体由匿名类的常规规则管理;特别是它不能包含任何构造函数。
这使得表达式i
有点模棱两可:我们指的是封闭实例的字段,还是内部实例的字段?唉,内部实例没有继承字段:
声明为private的类的成员不能被该类的子类继承。
因此,编译器得出结论,我们的意思是访问封闭实例的字段——但是在静态块中,是没有封闭实例,因此出现错误。
通过this
访问字段
B { public int get() { return this.i; } },
规范要求:
当用作主表达式时,关键字this表示一个值,该值是对调用实例方法的对象的引用(§15.12),或对正在构造的对象的引用。
因此,很明显我们需要的是内部类的字段,而不是外部类的字段。
编译器拒绝字段访问表达式this.i
的原因是:
声明为private的类的成员不能被该类的子类继承。
也就是说,私有字段只能通过声明该字段的类型的引用访问,而不能通过其子类型的引用访问。事实上,
B { public int get() { return ((E)this).i; } },
可以正常编译。
通过super
访问像这样,super引用调用该方法的对象(或正在构造的对象)。因此,很明显我们指的是内部实例。另外,super的类型是E
,因此声明是可见的。
通过其他字段访问
D { public int get() { return D.i; } };
这里,D
是对E
中声明的静态字段D
的非限定访问。因为它是一个静态字段,所以使用哪个实例的问题是没有意义的,并且访问是有效的。
非常脆弱,因为只有在枚举对象完全构造之后才分配字段。如果有人在构造过程中调用get(),就会抛出一个NullPointerException
。
正如我们所看到的,访问其他类型的私有字段受到一些复杂的限制。由于很少需要,开发人员可能不知道这些微妙之处。
虽然将字段设置为protected
会削弱访问控制(即允许包中的其他类访问该字段),但它可以避免这些问题。
看一下这段代码:
public class E
{
final int i;
private final int j;
final E a;
E() { i = j = 0; a = null; }
E(int p_i) {
this.i = this.j = p_i;
a = new E() {
int getI() { return i; }
int getJ() { return j; }
};
}
int getI() { throw new UnsupportedOperationException(); }
int getJ() { throw new UnsupportedOperationException(); }
public static void main(String[] args) {
final E ea = new E(1).a;
System.out.println(ea.getI());
System.out.println(ea.getJ());
}
}
这个打印0
1
和 i
和j
之间唯一的区别是访问级别!
更新
看起来确实如此,因为它是在静态块中定义的。看一下以下内容:
private E works = new E("A", 0) {
public int get() {
return i; // Compiles
}
};
static {
A = new E("A", 0) {
public int get() {
return i; // Doesn't Compile
}
};
}
原来
我编译了枚举,然后使用Jad对其进行反编译,看看代码可能是什么样子:
static abstract class E extends Enum
{
public static E[] values()
{
return (E[])$VALUES.clone();
}
public static E valueOf(String s)
{
return (E)Enum.valueOf(Foo$E, s);
}
public abstract int get();
public static final E A;
private int i;
private static final E $VALUES[];
static
{
A = new E("A", 0) {
public int get()
{
return A.i;
}
}
;
$VALUES = (new E[] {
A
});
}
private E(String s, int j)
{
super(s, j);
i = 0;
i = 1;
}
}
这使我更清楚,A
是一个匿名内部类,定义在类型为E
的静态初始化块中。在匿名内部类中寻找私有成员可见性,我在这个答案中发现了以下内容(为什么在匿名类中只有final变量可访问?):
当你创建一个匿名内部类的实例时,在该类中使用的任何变量的值都会通过自动生成的构造函数复制进来。这避免了编译器必须自动生成各种额外的类型来保存"局部变量"的逻辑状态,例如c#编译器所做的
从这里我认为A.i
指的是A中复制的变量,而不是E中声明的i
。在E中获得i
的唯一方法是如果它是静态的或不是私有的。
private
方法可以在嵌套类中访问,只要它们位于同一个类文件中。
因此,即使A是e的匿名子类,第一个例子也可以工作。第二个例子为什么不能编译,这很奇怪,但我怀疑它是误导错误消息,因为你可以做
A { public int get() { return super.i; } };
编译不过
A { public int get() { return i; } };
为
error: non-static variable i cannot be referenced from a static context
如果这是一个静态上下文,那么显然是不正确的super.i
将没有意义。
正如Marko所说
A { public int get() { return this.i; } };
产生错误消息
error: i has private access in E
可能更合适。也就是说,你可以显式访问这个字段,但不能隐式访问。