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

5
我希望了解一下我在处理匿名类时遇到的奇怪行为。
我有一个类,在其构造函数中调用了一个受保护的方法(我知道,这是一个糟糕的设计,但那是另外一个故事...)。
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类相当:编译器会自动创建一个新的继承A的类C$1,并创建实例变量来存储在匿名类方法中引用的本地变量,模拟闭包等等……

但是运行时却得到了不同结果。

> 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
...

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


为什么你认为你的第一段和第二段代码是相同的?在第二段代码中,你正在访问局部变量。这将在匿名类语句执行之前被初始化。 - Rohit Jain
嗯...好的,你说:编译器创建一个类来实现这种情况应该对开发者隐藏,所以C$1类是一个特例,如果它不遵循标准构造函数规则也没关系。这很合理,但在我看来还是有点尴尬。 - ugo
4个回答

3

您的匿名类实例与第一个代码片段的行为不同,因为您使用了一个在创建匿名类实例之前初始化值的局部变量。

如果您在匿名类中使用实例变量,可以获得类似于第一个代码片段的行为:

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尚未初始化。


我知道变量已经被初始化了,我的问题是关于查看字节码时,匿名类的构造函数不遵循其他“普通”类的规则。抱歉,也许我没有表达清楚,我编辑了问题... - ugo
这是Java语言中的常见问题:超类构造函数调用必须是构造函数中的第一个语句。在语言中,编译器通过错误来强制执行此操作。然而,字节码(JVM)却允许此操作,并且编译器在匿名类和可能其他地方也会利用它。 - Clashsoft

3

这个问题适用于所有内部类,不仅限于匿名类(匿名类是内部类)。

JLS并没有规定内部类如何访问外部局部变量;它只指定局部变量实际上是final的,并且在内部类体之前被明确定义。因此,可以推断出内部类必须看到局部变量的明确定义值。

JLS没有具体指定内部类如何看到该值;编译器可以使用任何技巧(在字节码级别上可能)来实现这种效果。特别地,这个问题与构造函数完全无关(就语言而言)。

一个类如何访问其外部实例也是一个相似的问题。这个问题略微复杂,并且与构造函数有一些关系。然而,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(); }

因此,我们将课程分为两部分;外部部分甚至在超级构造函数之前执行。这在某些情况下非常有用。(另一个例子)


(内部匿名访问本地变量封闭实例有效的最终超级构造函数)


+1 是因为内部类是一个相当不错的技巧。不过,在工厂方法中使用匿名类也足够了。 - Clashsoft
@Clashsoft - 你是对的;但如果出于某些原因需要一个命名的子类。 - ZhongYu

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计算,因为没有办法既将其传递给超级构造函数将其存储在实例变量中。

怎么样加一个 B(String),并且 B(i, d) 调用 this(computeComplexString(i, d)) - ZhongYu

1
这两个例子没有关联。
在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


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