我想了解我在处理匿名课程时遇到的奇怪行为。
我有一个类在其构造函数中调用受保护的方法(我知道,设计很差,但这是另一个故事......)
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
ctor之前被调用,然后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,也许在介绍lamdbas时,他们改变了匿名类的实现方式(你不再需要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
...
而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
....
有人能告诉我为什么会有这种差异吗? 有没有办法使用“普通”类复制匿名类的行为?
编辑:
澄清问题:为什么匿名类的初始化会破坏初始化任何其他类的规则(在设置任何其他变量之前调用超级构造函数)?
或者,有没有办法在inovking超级构造函数之前在B
类中设置实例变量?
答案 0 :(得分:3)
这个问题适用于所有内部类,而不仅仅是anon类。 (Anon类是内部类)
JLS没有规定内部类体如何访问外部局部变量;只有specifies局部变量才是最终的,并且在内部类体之前明确赋值。因此,内部类必须看到局部变量的明确赋值。
JLS没有详细说明内部类如何看到该值;编译器可以使用任何技巧(在字节码级别上可能)来实现这种效果。特别是,这个问题与构造函数完全无关(就语言而言)。
类似的问题是内部类如何访问外部实例。这有点复杂,它与构造函数有something。尽管如此,JLS仍然没有规定编译器如何实现它;该部分包含一个注释“...编译器可以表示它所希望的直接封闭的实例。不需要Java编程语言......”
从JMM的角度来看,这种规范不足可能是一个问题;目前还不清楚如何对内部类中的读取进行写入。可以合理地假设,写入是在合成变量上完成的,该合成变量在new InnerClass()
动作之前(按编程顺序);内部类读取合成变量以查看外部局部变量或封闭实例。
有没有办法使用“普通”类复制匿名类的行为?
您可以将“普通”类安排为外部内部类
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(); }
所以我们把班级分为两部分;外部部分甚至在超级构造函数之前执行。这在某些情况下很有用。 (another example)
(内部匿名访问局部变量,包含实例有效的最终超级构造函数)
答案 1 :(得分:2)
您的匿名类实例与第一个代码段的行为不同,因为您使用的是在创建匿名类实例之前初始化其值的局部变量。
如果在匿名类中使用实例变量,则可以使用匿名类实例获取与第一个代码段相似的行为:
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()
初始化之前A
的构造函数执行了avalue
。
答案 2 :(得分:2)
允许匿名类中的变量捕获破坏正常构造函数的规则(超级构造函数调用必须是第一个语句),因为此法则仅由编译器强制执行。 JVM允许在调用超级构造函数之前运行任何字节码,编译器本身使用它(它打破了自己的规则!)。
您可以使用内部类来模仿行为,如bayou.io的答案中所示,或者您可以在静态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
计算,因为无法将其传递给超级构造函数并且将其存储在实例变量中。
答案 3 :(得分:0)
这两个例子没有关系。
在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); }
}
正在打印的值是avalue
方法的局部变量main()
。