[讨论] 问一个内部类的问题

BaronZ 2013-01-15
RednaxelaFX 大神,打扰了,菜鸟问你一个问题。
最近在看Effective Java的书。看到内部类这一部分(Item 22)
里面说到:
内部类,如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时却仍然得以保留。

请问红色那部分怎么理解?能不能给个具体的代码演示一下? 如果我把那个外围实例直接指向null(即符合垃圾回收?),那这个外围实例也会得以保留吗?
RednaxelaFX 2013-01-15
BaronZ 写道
如果我把那个外围实例直接指向null(即符合垃圾回收?),那这个外围实例也会得以保留吗?

实例是不能null掉的,只有引用可以。Java语言不允许我们直接访问对象实例,而是必须通过引用来访问。

=========================================================

至于您原本的问题,很简单。
经典例子是容器与迭代器的关系。我们造个小例子看看:

public class MyList /* implements List */ {
  public class Iterator /* implements Iterator */ {
    public Iterator() {
      System.out.println("an instance of MyList$Iterator is created");
    }
    
    public void finalize() {
      System.out.println("an instance of MyList$Iterator will be collected");
    }
  }
  
  public MyList() {
    System.out.println("an instance of MyList is created");
  }
  
  public void finalize() {
    System.out.println("an instance of MyList will be collected");
  }
  
  public Iterator iterator() {
    return new Iterator();
  }
  
  public static void main(String[] args) throws Exception {
    MyList list = new MyList();
    Iterator iterator = list.iterator();
    
    list = null;
    System.out.println("============ before 1st GCs ============");
    System.gc();
    System.runFinalization();
    System.gc();
    System.out.println("============ after 1st GCs ============");
    
    iterator = null;
    System.out.println("============ before 2nd GCs ============");
    System.gc();
    System.runFinalization();
    System.gc();
    System.out.println("============ after 2nd GCs ============");
  }
}


运行的结果会是:
$ java MyList
an instance of MyList is created
an instance of MyList$Iterator is created
============ before 1st GCs ============
============ after 1st GCs ============
============ before 2nd GCs ============
an instance of MyList$Iterator will be collected
an instance of MyList will be collected
============ after 2nd GCs ============

Effective Java想说的就是表面上看起来所有指向MyList的引用都已死的时候,内部类Iterator的实例对外部类MyList的实例的隐含引用有可能使得MyList实例还是活的。
其实很好理解,那个隐含引用你只要看作是自己写的一个普通成员变量就好了,效果是一模一样的。

仔细看看MyList.Iterator这个内部类的构成可以看到javac编译器帮我们生成了一个隐含成员this$0用于指向外部MyList的实例。我们在源码里写的无参构造器也被编译器添加了一个MyList类型的参数到参数列表里。
Classfile /D:/temp/MyList$Iterator.class
  Last modified 2013-1-15; size 616 bytes
  MD5 checksum 0e8ea35ae46d83174981605ee147bdca
  Compiled from "MyList.java"
public class MyList$Iterator
  SourceFile: "MyList.java"
  InnerClasses:
       public #29= #7 of #27; //Iterator=class MyList$Iterator of class MyList
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER

Constant pool:
   #1 = Fieldref           #7.#19         //  MyList$Iterator.this$0:LMyList;
   #2 = Methodref          #8.#20         //  java/lang/Object."<init>":()V
   #3 = Fieldref           #21.#22        //  java/lang/System.out:Ljava/io/PrintStream;
   #4 = String             #23            //  an instance of MyList$Iterator is created
   #5 = Methodref          #24.#25        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = String             #26            //  an instance of MyList$Iterator will be collected
   #7 = Class              #28            //  MyList$Iterator
   #8 = Class              #31            //  java/lang/Object
   #9 = Utf8               this$0
  #10 = Utf8               LMyList;
  #11 = Utf8               <init>
  #12 = Utf8               (LMyList;)V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               finalize
  #16 = Utf8               ()V
  #17 = Utf8               SourceFile
  #18 = Utf8               MyList.java
  #19 = NameAndType        #9:#10         //  this$0:LMyList;
  #20 = NameAndType        #11:#16        //  "<init>":()V
  #21 = Class              #32            //  java/lang/System
  #22 = NameAndType        #33:#34        //  out:Ljava/io/PrintStream;
  #23 = Utf8               an instance of MyList$Iterator is created
  #24 = Class              #35            //  java/io/PrintStream
  #25 = NameAndType        #36:#37        //  println:(Ljava/lang/String;)V
  #26 = Utf8               an instance of MyList$Iterator will be collected
  #27 = Class              #38            //  MyList
  #28 = Utf8               MyList$Iterator
  #29 = Utf8               Iterator
  #30 = Utf8               InnerClasses
  #31 = Utf8               java/lang/Object
  #32 = Utf8               java/lang/System
  #33 = Utf8               out
  #34 = Utf8               Ljava/io/PrintStream;
  #35 = Utf8               java/io/PrintStream
  #36 = Utf8               println
  #37 = Utf8               (Ljava/lang/String;)V
  #38 = Utf8               MyList
{
  final MyList this$0;
    flags: ACC_FINAL, ACC_SYNTHETIC


  public MyList$Iterator(MyList);
    flags: ACC_PUBLIC

    Code:
      stack=2, locals=2, args_size=2
         0: aload_0       
         1: aload_1       
         2: putfield      #1                  // Field this$0:LMyList;
         5: aload_0       
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: ldc           #4                  // String an instance of MyList$Iterator is created
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        17: return        
      LineNumberTable:
        line 3: 0
        line 4: 9
        line 5: 17

  public void finalize();
    flags: ACC_PUBLIC

    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String an instance of MyList$Iterator will be collected
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return        
      LineNumberTable:
        line 8: 0
        line 9: 8
}


再来看看MyList.iterator()方法的内容:
  public MyList$Iterator iterator();
    flags: ACC_PUBLIC

    Code:
      stack=3, locals=1, args_size=1
         0: new           #6                  // class MyList$Iterator
         3: dup           
         4: aload_0       
         5: invokespecial #7                  // Method MyList$Iterator."<init>":(LMyList;)V
         8: areturn       
      LineNumberTable:
        line 21: 0

可以看到实际程序比表面上的Java源码多传了一个MyList类型的引用给新创建的Iterator,这就是在传递外部实例的引用。
BaronZ 2013-01-15
@RednaxelaFX 感谢大神这么详细的回复。我等会消化消化。先表达一下激动之情:
你造吗?这感觉就像在微博上得到明星的回复一样。

RednaxelaFX 写道
BaronZ 写道
如果我把那个外围实例直接指向null(即符合垃圾回收?),那这个外围实例也会得以保留吗?

实例是不能null掉的,只有引用可以。Java语言不允许我们直接访问对象实例,而是必须通过引用来访问。

=========================================================

啊。。这个激动了,我是知道的。我想表达的是把引用指向null

MyList这个例子给得很好,看明白了过程了。list这个引用指向null后,因为还被Iterator实例引用着,所以即使调用gc也回收不了。btw,这么一说,这个是不是和js的闭包有点像?


内部类Iterator的实例对外部类MyList的实例的隐含引用有可能使得MyList实例还是活的
(还只看到上在的这句话,下面的还没看,先问问题)
为什么说“有可能”,有意外情况吗?

RednaxelaFX 写道

其实很好理解,那个隐含引用你只要看作是自己写的一个普通成员变量就好了,效果是一模一样的。

这个怎么说?不理解  
BaronZ 2013-01-15
再顺带问一个菜鸟问题:

接口里面的接口默认是不是public static 修饰的?
比如Map里面的Entry接口,源码里面是没有修饰符的。
RednaxelaFX 2013-01-15
BaronZ 写道
btw,这么一说,这个是不是和js的闭包有点像?

本来对象和闭包就有相似之处的,取决于你怎么看:http://rednaxelafx.iteye.com/blog/245022

BaronZ 写道
内部类Iterator的实例对外部类MyList的实例的隐含引用有可能使得MyList实例还是活的
(还只看到上在的这句话,下面的还没看,先问问题)
为什么说“有可能”,有意外情况吗?

如果Iterator的实例也死了那它有没有引用外部MyList的实例都没关系了。仅此而已

BaronZ 写道
RednaxelaFX 写道
其实很好理解,那个隐含引用你只要看作是自己写的一个普通成员变量就好了,效果是一模一样的。

这个怎么说?不理解  

就像这样:
public class MyList {
  public static class Iterator {
    private MyList this$0;

    public Iterator(MyList enclosingList) {
      this$0 = enclosingList;
    }
  }

  public Iterator iterator() {
    return new Iterator(this);
  }
}
RednaxelaFX 2013-01-15
BaronZ 写道
再顺带问一个菜鸟问题:

接口里面的接口默认是不是public static 修饰的?
比如Map里面的Entry接口,源码里面是没有修饰符的。

是的。请参考Java语言规范:http://docs.oracle.com/javase/specs/jls/se7/html/jls-9.html#jls-9.5
引用
Interfaces may contain member type declarations (§8.5).

A member type declaration in an interface is implicitly static and public. It is permitted to redundantly specify either or both of these modifiers.
BaronZ 2013-01-16
RednaxelaFX 写道

本来对象和闭包就有相似之处的,取决于你怎么看:http://rednaxelafx.iteye.com/blog/245022

这个英语只理解了字面意思,什么宗教性的还不懂,不过下面那个例子不错

感谢R大的回复,太正了

再问一个菜鸟问题?
我们都知道String a = "a"; String b = "a";-->a==b
这里生成字符串时,会去字符串池找"a",决定是生成还是返回存在的。
最近看到这个Object o1 = "a"; Object o2 = "a";-->o1==o2
请问Object这个也可以像上面那么解释吗?有没有什么特殊的地方?
BaronZ 2013-01-16
再问一个:为什么要两个gc?或者只跑一个runFinalization应该也是可以?
    System.gc();   
    System.runFinalization();   
    System.gc(); 
RednaxelaFX 2013-01-16
BaronZ 写道
再问一个菜鸟问题?
我们都知道String a = "a"; String b = "a";-->a==b
这里生成字符串时,会去字符串池找"a",决定是生成还是返回存在的。
最近看到这个Object o1 = "a"; Object o2 = "a";-->o1==o2
请问Object这个也可以像上面那么解释吗?有没有什么特殊的地方?

Java里变量有类型,值也有类型。

对原始类型的变量而言,它自身的类型跟它所能持有的值的类型必然是完全一致的,例如说int类型的变量就存着int类型的值。

但对引用类型来说就多一层间接。引用类型的变量的类型跟它所能持有的值的类型仍然是完全一致的,例如说Foo类型的变量就持有Foo类型的引用;但是引用是允许指向“兼容类型”的,例如说Object类型的引用兼容于Foo类型的对象,所以可以有 Object obj = new Foo(); 。这样一来引用类型的变量跟最终指向的对象的类型就不一定完全一致了,而是只要兼容即可。

(所谓兼容就是更宽泛的类型的引用可以指向更具体的类型的对象。类的话可以用继承深度来定义,把java.lang.Object的继承深度定义为0,每继承一层就定义它的继承深度是上一层+1。那么在同一条继承链上,继承深度浅的就更宽泛,继承深度深的就更具体。接口同理)

所以回到您原本的问题,实际上String a和Object o1这俩变量里的值会是一样的,都指向同一个对象;引用的==比较的是引用值的相等性,也就是是否指向同一对象。这样可以理解了么?

BaronZ 写道
再问一个:为什么要两个gc?或者只跑一个runFinalization应该也是可以?
    System.gc();   
    System.runFinalization();   
    System.gc(); 

跑一个System.gc()加一个System.runFinalization()也行,对这个例子想做的演示没啥影响。我只是想在例子里保证垃圾清理干净了而已。

第一次GC的时候有finalizer的对象如果被发现已经没有活引用就会进入finalization队列;
runFinalization()保证我们等所有当前要finalize的对象都处理完了才继续运行下去;
这个时候那些刚finalize完的对象才真正成为了垃圾,于是第二次GC可以把它们回收掉。
BaronZ 2013-01-16
RednaxelaFX 写道

所以回到您原本的问题,实际上String a和Object o1这俩变量里的值会是一样的,都指向同一个对象;引用的==比较的是引用值的相等性,也就是是否指向同一对象。这样可以理解了么?

懂了!再次感谢RednaxelaFX,事无巨细啊  
Global site tag (gtag.js) - Google Analytics