为啥内部类的扩展会得到重复的外部类引用?

Posted

技术标签:

【中文标题】为啥内部类的扩展会得到重复的外部类引用?【英文标题】:Why do extensions of inner classes get duplicate outer class references?为什么内部类的扩展会得到重复的外部类引用? 【发布时间】:2013-12-31 01:27:55 【问题描述】:

我有以下 Java 文件:

class Outer 
    class Inner  public int foo; 
    class InnerChild extends Inner 

我使用以下命令编译然后反汇编文件:

javac test.java && javap -p -c Outer Outer.Inner Outer.InnerChild

这是输出:

Compiled from "test.java"
class Outer 
  Outer();
    Code:
       0: aload_0
       1: invokespecial #1            // Method java/lang/Object."<init>":()V
       4: return

Compiled from "test.java"
class Outer$Inner 
  public int foo;

  final Outer this$0;

  Outer$Inner(Outer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1            // Field this$0:LOuter;
       5: aload_0
       6: invokespecial #2            // Method java/lang/Object."<init>":()V
       9: return

Compiled from "test.java"
class Outer$InnerChild extends Outer$Inner 
  final Outer this$0;

  Outer$InnerChild(Outer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1            // Field this$0:LOuter;
       5: aload_0
       6: aload_1
       7: invokespecial #2            // Method Outer$Inner."<init>":(LOuter;)V
      10: return

第一个内部类有其this$0 字段,指向Outer 的实例。没关系。第二个内部类继承了第一个,有一个同名的重复字段,它在调用具有相同值的超类的构造函数之前对其进行初始化。

上面int foo 字段的目的只是为了确认从超类继承的字段不会出现在子类反汇编的javap 输出中。

第一个this$0 字段不是私有的,所以InnerChild 应该可以使用它。额外的字段似乎只是浪费内存。 (我首先使用内存分析工具发现了它。)它的目的是什么?有什么办法可以摆脱它?

【问题讨论】:

我不知道为什么需要该字段。如果它对您很重要,您可以尝试将其从字节码中删除。 内部类是一个编译器hack。没有理由期望它以这种方式“优化”。 (我怀疑编译器的人害怕接触代码,因为害怕破坏它。) 【参考方案1】:

这两个类可能不是同一个类的内部类(如果你有一个复杂的层次结构),所以确实存在两个引用不同的情况。

例如:

 class Outer 

     class InnerOne 
      

     class Wrapper 
         class InnerTwo extends InnerOne 
         
     
 

InnerTwo 引用 WrapperInnerOne 引用 Outer。

您可以使用以下 Java 代码进行尝试:

public class Main

    static class Outer 

        class InnerOne 
             String getOuters() 
                return this+"->"+Outer.this;
             
         

        class Wrapper 
           class InnerTwo extends InnerOne 
             String getOuters() 
               return this+"->"+Wrapper.this+"->"+super.getOuters();
             
           
        
    

    public static void main(String[] args)
       Outer o = new Outer();
       Outer.Wrapper w = o.new Wrapper();
       Outer.Wrapper.InnerTwo i2 = w.new InnerTwo();
       System.out.println(w);
       System.out.println(i2);
       System.out.println(i2.getOuters());
    


我已在 tryjava8 上将其设置为 sn-p:http://www.tryjava8.com/app/snippets/52c23585e4b00bdc99e8a96c

Main$Outer$Wrapper@1448139f 
Main$Outer$Wrapper$InnerTwo@1f7f1d70 
Main$Outer$Wrapper$InnerTwo@1f7f1d70->Main$Outer$Wrapper@1448139f->Main$Outer$Wrapper$InnerTwo@1f7f1d70->Main$Outer@6945af95

您可以看到两个getOuters() 调用引用了不同的对象。

【讨论】:

我支持你,因为这个案例很有趣,让我思考。不过,关键短语是“如果你有一个复杂的层次结构”——我没有! 但是 Java 框架必须处理复杂的层次结构,因此即使在您的特定情况下此时不需要它们,也需要这些字段。 (毕竟它永远不知道将来是否会从您的类中创建新的子类)。 二进制兼容性很棘手,但我想不出额外字段修复的特定兼容性问题。新的子类会产生什么影响?我认为它应该可以应付。【参考方案2】:

@TimB 的回答很有趣,因为它显示了外部类引用的微妙之处,但我想我找到了一个更简单的案例:

class Outer 
    class Inner 


class OuterChild extends Outer 
    class InnerChild extends Inner 

在这种情况下,InnerChild 的 this$0 引用的类型是 Child 而不是 Outer,因此它不能使用来自其父级的 this$0 字段,因为即使值相同,它也是错误的类型。我怀疑这是额外字段的起源。不过,我相信当 InnerChild 和 Inner 具有相同的外部类时,Javac 可以消除它,因为这样该字段具有相同的值和相同的类型。 (我会将其报告为错误,但他们从不修复它们,如果不是错误,他们也不会提供反馈。)

不过,我终于找到了一些不错的解决方法。

最简单的解决方法(我花了很长时间才想到)是使成员类“静态”,并将 ref 存储到 Outer 作为基类上的显式字段:

class Outer 
    static class Inner 
        protected final Outer outer;

        Inner(Outer outer) 
            this.outer = outer;
        
    

    static class InnerChild extends Inner 
        InnerChild(Outer outer) 
            super(outer);
        
    

通过outer 字段,Inner 和 InnerChild 都可以访问 Outer 的实例成员(甚至是私有的)。这相当于我认为 Javac 应该为原始非静态类生成的代码。

第二种解决方法:我完全偶然发现非静态成员类的子类可能是静态的!这些年在 Java 中,我从来不知道使它工作的晦涩语法......它不在官方教程中。我一直认为这是不可能的,除非 Eclipse 自动为我填写了构造函数。 (不过,Eclipse 似乎不想重复这个魔法……我好糊涂。)反正,显然它被称为“合格的超类构造函数调用”,它是buried in the JLS in section 8.8.7.1。

class Outer 
    class Inner 
        protected final Outer outer()  return Outer.this; 
    

    static class InnerChild extends Inner 
        InnerChild(Outer outer) 
            outer.super(); // wow!
        
    

这与之前的解决方法非常相似,除了 Inner 是一个实例类。 InnerChild 避免了重复的 this$0 字段,因为它是静态的。 Inner 提供了一个 getter,以便 InnerChild 可以访问对 Outer 的引用(如果它愿意)。这个 getter 是最终的且非虚拟的,因此对于 VM 内联来说是微不足道的。

第三种变通方法是仅将外部类 ref 存储在 InnerChild 中,并让 Inner 通过虚拟方法访问它:

class Outer 
    static abstract class Inner 
        protected abstract Outer outer();
    

    class InnerChild extends Inner 
        @Override protected Outer outer()  return Outer.this; 
    

这可能不太有用(?)。一个优点是它让 InnerChild 有一个更简单的构造函数调用,因为对其封闭类的引用通常会隐式传递给它。如果 InnerChild 有一个不同的封闭类(OuterChild,扩展 Outer),它也可能是一个模糊的优化,它需要访问的成员比 Inner 需要访问 Outer 的成员更频繁——它允许 InnerChild 使用 OuterChild 的成员而无需强制转换,但需要 Inner 调用虚方法来访问 Outer 的成员。

在实践中,所有这些变通方法都有点麻烦,所以只有当你有成千上万个这样的类实例时它们才值得(我就是这样做的!)。


更新!

刚刚意识到“合格的超类构造函数调用”是 Javac 为何首先生成重复字段的一大难题。对于 InnerChild 的一个实例,扩展 Inner,两个 Outer 的成员类,有可能有一个与其超类认为是封闭实例不同的封闭实例

class Outer 
    class Inner 
        Inner() 
            System.out.println("Inner's Outer: " + Outer.this);
        
    

    class InnerChild extends Inner 
        InnerChild() 
            (new Outer()).super();
            System.out.println("InnerChild's Outer: " + Outer.this);
        
    


class Main 
    public static void main(String[] args) 
        new Outer().new InnerChild();
    

输出:

Inner's Outer: Outer@1820dda
InnerChild's Outer: Outer@15b7986

我仍然认为通过一些努力,Javac 可以消除常见情况下的重复字段,但我不确定。这肯定是一个比我最初想象的更复杂的问题。

【讨论】:

是的,这是相同效果的一个很好的例子,你是对的,使内部类静态是删除引用的方法 - 假设你不需要该类中的引用。跨度>

以上是关于为啥内部类的扩展会得到重复的外部类引用?的主要内容,如果未能解决你的问题,请参考以下文章

Java内部类持有外部类的引用详细分析与解决方案

java内部类如何被外部引用

Java内部类

内部类和静态内部类有什么区别?

java 内部类为啥不能static

java 内部类和外部类的关系