【问题标题】:Java: initialization and costructor of anonymous classesJava:匿名类的初始化和构造函数
【发布时间】:2015-09-08 17:13:46
【问题描述】:

我想了解我在处理匿名类时遇到的一种奇怪行为。

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

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

谁能告诉我为什么会有这种差异? 有没有办法使用“普通”类来复制匿名类的行为?

编辑: 澄清问题:为什么匿名类的初始化会破坏任何其他类的初始化规则(在设置任何其他变量之前调用超级构造函数)? 或者,有没有办法在调用超级构造函数之前在B 类中设置实例变量?

【问题讨论】:

  • 为什么你认为你的第一个和第二个代码是一样的?在第二个代码中,您正在访问局部变量。这将在您的匿名类语句执行之前被初始化。
  • 嗯...好吧,你说:编译器创建一个类来实现这个场景的事实应该对开发人员隐藏,所以C$1类是一个特例,如果它不遵循标准的构造函数规则。这是相当合理的,但是,恕我直言,这有点尴尬。

标签: java constructor javac anonymous-class javap


【解决方案1】:

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

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 因为内部类是一个很好的技巧。不过,工厂方法中的匿名类就足够了。
  • @Clashsoft - 你是对的;但如果出于某种原因需要一个命名的子类。
【解决方案2】:

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

如果您在匿名类中使用实例变量,则可以通过匿名类实例获得与第一个 sn-p 类似的行为:

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的构造函数执行。

【讨论】:

  • 我知道变量已经初始化了,我的问题是看字节码,匿名类的构造函数不遵循其他“正常”类的规则。对不起,也许我不清楚,我编辑了问题...
  • 这是Java 语言 中的一个常见问题:超级构造函数调用必须是构造函数中的第一条语句。 在语言中,编译器通过错误强制执行此操作。然而,字节码 (JVM) 允许这样做,并且编译器在匿名类和可能的其他地方也使用它。
【解决方案3】:

匿名类中的变量捕获是允许打破正常构造函数的规则(超级构造函数调用必须是第一条语句),因为这个规律只由编译器强制执行。 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))
【解决方案4】:

这两个例子不相关。

在 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-07
    • 2014-10-23
    • 1970-01-01
    • 2013-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多