美文网首页
关于匿名内部类构造函数

关于匿名内部类构造函数

作者: 7d972d5e05e8 | 来源:发表于2020-06-12 16:00 被阅读0次

    一、查看匿名内部类的字节码

    1. 写一个匿名内部类的实现,然后编译下
    2. 看到该类路径下,会多一个XXX$1.class
    3. 使用javap -v xxx$.class

    注意这里一定要保留"",否则javap找到的是原始类

    二、源码分析

    我们先贴一段dubbo的JavassistProxyFactory类的源码,引出今天的问题:

    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
            // TODO Wrapper类不能正确处理带$的类名
            final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
            return new AbstractProxyInvoker<T>(proxy, type, url) {
                @Override
                protected Object doInvoke(T proxy, String methodName,
                                          Class<?>[] parameterTypes,
                                          Object[] arguments) throws Throwable {
                    return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
                }
            };
        }
    

    仔细观察下这段代码,getInvoker是dubbo启动时的初始化过程。该方法只会在初始化执行一次,现在提出我们的疑问。wrapper实例在方法中属于局部变量,那么getInvoker执行结束后,方法栈销毁,wrapper实例应该要被jvm回收。那么dubbo在执行调用时,会执行到doInvoke方法,而这个AbstractProxyInvoker匿名内部类的构造方法并没有把wrapper传递到它的构造方法里面,doInvoker是怎么调用到wrapper的呢?

    带着疑问,我们先来猜测下new AbstractProxyInvoker<T>(proxy, type, url)的构造方法,只会把proxy, type, url三个变量初始化给AbstractProxyInvoker实现类,那wrapper呢?肯定在某个地方也赋值给它了。为了验证猜想,我们只能抓取字节码查看了。
    使用HSDB抓取到的字节码,分析如下图所示:


    image.png

    看到void <init>这个方法了,它就是构造方法。数一数它的参数:5个参数,是不是比代码中的3个多出来2个。这两个看下上面的Fields,分别对应到参数列表的第1个和第5个。
    所以得出结论:匿名内部类默认生成了新的构造函数,新构造函数会把final变量设置到构造方法里面。
    我们看下完整的匿名内部类的字节码:

    Compiled from "JavassistProxyFactory.java"
    class com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1 extends com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker<T> {
      //字段1
      final com.alibaba.dubbo.common.bytecode.Wrapper val$wrapper;
      //字段2
      final com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory this$0;
    //新构造函数,或者匿名实现类的构造函数
    com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1(com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory, java.lang.Object, java.lang.Class, com.alibaba.dubbo.common.URL, com.alibaba.dubbo.common.bytecode.Wrapper);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #1                  // Field this$0:Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory;
           5: aload_0
           6: aload         5
           8: putfield      #2                  // Field val$wrapper:Lcom/alibaba/dubbo/common/bytecode/Wrapper;
          11: aload_0
          12: aload_2
          13: aload_3
          14: aload         4
          16: invokespecial #3                  // Method com/alibaba/dubbo/rpc/proxy/AbstractProxyInvoker."<init>":(Ljava/lang/Object;Ljava/lang/Class;Lcom/alibaba/dubbo/common/URL;)V
          19: return
        LineNumberTable:
          line 41: 0
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      20     0  this   Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory$1;
              0      20     1 this$0   Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory;
              0      20     2 proxy   Ljava/lang/Object;
              0      20     3  type   Ljava/lang/Class;
              0      20     4   url   Lcom/alibaba/dubbo/common/URL;
    
      protected java.lang.Object doInvoke(T, java.lang.String, java.lang.Class<?>[], java.lang.Object[]) throws java.lang.Throwable;
        Code:
           0: aload_0
           1: getfield      #2                  // Field val$wrapper:Lcom/alibaba/dubbo/common/bytecode/Wrapper;
           4: aload_1
           5: aload_2
           6: aload_3
           7: aload         4
           9: invokevirtual #4                  // Method com/alibaba/dubbo/common/bytecode/Wrapper.invokeMethod:(Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object;
          12: areturn
        LineNumberTable:
          line 46: 0
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      13     0  this   Lcom/alibaba/dubbo/rpc/proxy/javassist/JavassistProxyFactory$1;
              0      13     1 proxy   Ljava/lang/Object;
              0      13     2 methodName   Ljava/lang/String;
              0      13     3 parameterTypes   [Ljava/lang/Class;
              0      13     4 arguments   [Ljava/lang/Object;
    }
    

    三、本地验证

    首先贴出来咱们的匿名接口,如下:

    public interface MyInterface {
        int hello();
        int helloWorld();
    }
    

    再贴出来实现:

    public class AnonyInnerClassTest {
        public static void main(String[] args){
            final ClassB  b = new ClassB(2);
            final ClassB  c = new ClassB(2);
            MyInterface target = new MyInterface(){
                @Override
                public int hello() {
                    b.setA(3);
                    return 0;
                }
                @Override
                public int helloWorld() {
                    c.getA();
                    return 1;
                }
            };
            target.hello();
        }
    }
    

    观察下代码,我们只在方法里面用了b和c变量。但是就是这种使用,影响了构造函数的生成。我们看下他的字节码:

    Compiled from "AnonyInnerClassTest.java"
    class com.example.demo.object.AnonyInnerClassTest$1 implements com.example.demo.object.MyInterface {
      com.example.demo.object.AnonyInnerClassTest$1(com.example.demo.object.ClassB, com.example.demo.object.ClassB);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #13                 // Field val$b:Lcom/example/demo/object/ClassB;
           5: aload_0
           6: aload_2
           7: putfield      #15                 // Field val$c:Lcom/example/demo/object/ClassB;
          10: aload_0
          11: invokespecial #17                 // Method java/lang/Object."<init>":()V
          14: return
        LineNumberTable:
          line 1: 0
          line 14: 10
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      15     0  this   Lcom/example/demo/object/AnonyInnerClassTest$1;
    
      public int hello();
        Code:
           0: aload_0
           1: getfield      #13                 // Field val$b:Lcom/example/demo/object/ClassB;
           4: iconst_3
           5: invokevirtual #26                 // Method com/example/demo/object/ClassB.setA:(I)V
           8: iconst_0
           9: ireturn
        LineNumberTable:
          line 17: 0
          line 18: 8
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      10     0  this   Lcom/example/demo/object/AnonyInnerClassTest$1;
    
      public int helloWorld();
        Code:
           0: aload_0
           1: getfield      #15                 // Field val$c:Lcom/example/demo/object/ClassB;
           4: invokevirtual #33                 // Method com/example/demo/object/ClassB.getA:()I
           7: pop
           8: iconst_1
           9: ireturn
        LineNumberTable:
          line 23: 0
          line 24: 8
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      10     0  this   Lcom/example/demo/object/AnonyInnerClassTest$1;
    }
    
    

    可以看到它的字节码中,构造函数AnonyInnerClassTest$1(com.example.demo.object.ClassB, com.example.demo.object.ClassB),里面有两个参数,分别对应b和c。
    验证了我们的猜想,在编译阶段匿名内部类被编译器解开语法糖,编译器为匿名类的实现类生成类名,构造函数信息。构造函数的生成,依赖其实现方法用到的所有final申明的对象。

    至于为啥要final,网上一大堆,自己可以去看看。这里我说下我的理解吧。

    通过上面构造函数的分析,如果内部类使用了局部变量,那么局部变量就变成了匿名内部类实现类的成员变量。

    1. 从变量的生命周期来看。

    本来局部变量的生命周期就是这个方法内部,方法执行结束就销毁了。但是变成了成员变量后,它的生命周期就大大提高了,和实例的生命周期一致了。如果在这么长的生命周期内,且该对象不小心变成多线程共享的,那么每个方法都可以随意修改它的值,会导致这个变量的调用非常混乱,多线程不安全。当然引用对象修饰为final也没用,里面的东西还是能改的,这是另外一回事。

    2.如果不用final修饰,那么在局部变量生命周期内其他时间,这个变量被修改了。那么匿名内部类的实现类持有的相同的对象,到底要不要跟着修改呢?答案是匿名内部类在申明的那一刻起,它持有的对象是通过copy方式达到一致。后面原变量的修改,不会影响到匿名内部类。

    1. 为什么要拷贝呢?
      现在我们知道了,是由于一个拷贝的动作,使得内外两个变量无法实时同步,其中一方修改,另外一方都无法同步修改,因此要加上final限制变量不能修改。那么为什么要拷贝呢,不拷贝不就没那么多事了吗?这时候就得考虑一下Java虚拟机的运行时数据区域了,原始的局部变量是位于方法内部的,因此它是在虚拟机栈上,也就意味着这个变量无法进行共享,匿名内部类也就无法直接访问,因此只能通过值传递的方式,传递到匿名内部类中。
      所以必须使用final来避免这种混乱。

    参考文章:匿名内部类访问局部变量加final修饰的问题

    到这里,终于解开了dubbo那段源码wrapper局部变量在getInvoker执行完成后,为啥它没有被回收。

    至于匿名内部类为什么要这么做?我猜测如果不这么做,那么开发者就要定义一个新类,然后自己写构造函数,自己把所有实现方法里面要用到的对象都传递进这个构造函数。编译器觉得这种模式完全可以由它来自动提供,所以就提供了这么个语法糖,解放开发者。

    相关文章

      网友评论

          本文标题:关于匿名内部类构造函数

          本文链接:https://www.haomeiwen.com/subject/sjfntktx.html