Java:匿名类的初始化和构造函数

时间:2015-09-08 17:13:46

标签: java constructor javac anonymous-class javap

我想了解我在处理匿名课程时遇到的奇怪行为。

我有一个类在其构造函数中调用受保护的方法(我知道,设计很差,但这是另一个故事......)

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类中设置实例变量?

4 个答案:

答案 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()