为什么内部类的扩展会获得重复的外部类引用?

时间:2013-12-31 01:27:55

标签: java inner-classes

我有以下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应该能够使用它。额外的字段似乎浪费了记忆。 (我首先使用记忆分析工具发现它。)它的目的是什么,有没有办法摆脱它?

2 个答案:

答案 0 :(得分:2)

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

例如:

 class Outer {

     class InnerOne {
     } 

     class Wrapper {
         class InnerTwo extends InnerOne {
         }
     }
 }

InnerTwo引用了WrapperInnerOne引用了外部。

您可以使用以下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上的代码段: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()调用引用了另一个对象。

答案 1 :(得分:1)

@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可以访问ref到Outer(如果它想要的话)。这个getter是final和non-virtual,因此VM内联是微不足道的。

第三种解决方法是仅在InnerChild中存储外部类ref,并让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),它可能也是一个模糊的优化,它需要访问的成员比内部需要访问的成员更多 - 它允许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可以消除常见情况下的重复字段,但我不确定。这肯定是一个比我初步想象的更复杂的问题。