我想了解我在处理匿名类时遇到的一个奇怪的行为。
我有一个类,调用一个受保护的方法内部的构造函数(我知道,糟糕的设计,但那是另一个故事…)
public class A {
public A() {
init();
}
protected void init() {}
}
然后我有另一个类扩展A
并覆盖init()
。
public class B extends A {
int value;
public B(int i) {
value = i;
}
protected void init() {
System.out.println("value="+value);
}
}
如果我写
B b = new B(10);
我> value=0
,这是预期的,因为超类的构造函数在B
函数之前被调用,然后value
仍然是。
但是当使用像这样的匿名类时
class C {
public static void main (String[] args) {
final int avalue = Integer.parsetInt(args[0]);
A a = new A() {
void init() { System.out.println("value="+avalue); }
}
}
}
我希望得到value=0
,因为这应该或多或少等于类B
:编译器自动创建一个新的类C$1
,扩展A
,并创建实例变量来存储在匿名类的方法中引用的局部变量,模拟闭包等…
但是当你运行这个时,我得到
> java -cp . C 42
> value=42
最初我认为这是由于我正在使用java 8,也许,当引入lambda时,它们改变了匿名类在引子下实现的方式(您不再需要final
),但我也尝试了java 7并得到了相同的结果…
实际上,看看javap
的字节码,我可以看到B
是
> javap -c B
Compiled from "B.java"
public class B extends A {
int value;
public B(int);
Code:
0: aload_0
1: invokespecial #1 // Method A."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field value:I
9: return
...
while for C$1
:
> javap -c C$1
Compiled from "C.java"
final class C$1 extends A {
final int val$v;
C$1(int);
Code:
0: aload_0
1: iload_1
2: putfield #1 // Field val$v:I
5: aload_0
6: invokespecial #2 // Method A."<init>":()V
9: return
....
谁能告诉我为什么有这种差异?是否有一种方法可以使用"正常"类来复制匿名类的行为?
编辑:为了澄清这个问题:为什么匿名类的初始化违反了任何其他类初始化的规则(在设置任何其他变量之前调用超构造函数)?或者,在调用超级构造函数之前是否有一种方法可以在B
类中设置实例变量?
这个问题适用于所有内部类,而不仅仅是匿名类。(匿名类是内部类)
JLS没有规定内部类体如何访问外部局部变量;它只指定局部变量是有效的final,并且在内部类主体之前明确赋值。因此,内部类必须看到局部变量的明确赋值,这是理所当然的。
JLS没有指定内部类如何看到该值;这取决于编译器使用任何技巧(这在字节码级别上是可能的)来实现该效果。特别是,这个问题与构造函数完全无关(就语言而言)。
一个类似的问题是内部类如何访问外部实例。这有点复杂,它确实与构造函数有关。尽管如此,JLS仍然没有规定编译器如何实现它;该部分包含一个注释"…编译器可以随心所欲地表示立即封闭的实例。Java编程语言没有必要…"
从JMM的角度来看,这种规格不足可能是一个问题;在inner class中,写与读之间的关系是如何完成的尚不清楚。可以合理地假设,写入是在一个合成变量上完成的,它在(按照编程顺序)new InnerClass()
动作之前;内部类读取合成变量以查看外部局部变量或封闭实例。
是否有一种方法可以使用"正常"类来复制匿名类的行为?
你可以将"normal"类安排为外部-内部类
public class B0
{
int value;
public B0(int i){ value=i; }
public class B extends A
{
protected void init()
{
System.out.println("value="+value);
}
}
}
它将像这样使用,打印10
new B0(10).new B();
可以添加一个方便的工厂方法来隐藏语法的丑陋
newB(10);
public static B0.B newB(int arg){ return new B0(arg).new B(); }
所以我们把班级分成两部分;外部部分甚至在超级构造函数之前执行。这在某些情况下很有用。(一个例子)
(内部匿名访问局部变量封装实例有效的最终超级构造函数)
你的匿名类实例的行为与你的第一个代码片段不同,因为你使用的是一个局部变量,其值在匿名类实例创建之前被初始化。
如果在匿名类中使用实例变量,可以得到与第一个匿名类实例类似的行为:
class C {
public static void main (String[] args) {
A a = new A() {
int avalue = 10;
void init() { System.out.println("value="+avalue); }
}
}
}
打印
value=0
因为init()
是在avalue
初始化之前由A
的构造函数执行的。
匿名类中的变量捕获被允许打破常规构造函数的规则(超构造函数调用必须是第一个语句),因为这个规则只由编译器强制执行。JVM允许在调用超级构造函数之前运行任何字节码,这是编译器自己用于匿名类的(它打破了自己的规则!)。
可以用内部类来模拟这种行为,如bayou所示。或者您可以在静态B
工厂方法中使用匿名方法:
public class B extends A
{
public static B create(int value)
{
return new B() {
void init() { System.out.println("value="+value);
};
}
}
这个限制实际上是毫无意义的,在某些情况下可能会很烦人:
class A
{
private int len;
public A(String s)
{
this.len = s.length();
}
}
class B extends A
{
private String complexString;
public B(int i, double d)
{
super(computeComplexString(i, d));
this.complexString = computeComplexString(i, d);
}
private static String computeComplexString(int i, double d)
{
// some code that takes a long time
}
}
在本例中,必须执行两次computeComplexString
计算,因为没有办法既将其传递给超级构造函数,又将存储在实例变量中。
这两个例子并不相关。
在B的例子中:
protected void init() {
System.out.println("value="+value);
}
打印的值是b实例的value
字段。
在匿名示例中:
final int avalue = Integer.parsetInt(args[0]);
A a = new A() {
void init() { System.out.println("value="+avalue); }
}
打印的值是main()
方法的局部变量avalue