朝花夕拾——Java的synthetic修饰词

1,142 阅读4分钟
原文链接: blog.csdn.net

Ok,目前为止,我还只是Android coder,一切对其他的学习都是以Android 为主线的支线任务。所以为什么会提到这个方法呢?是来自于google官方的性能建议文档的这样一句话:

Consider Package Instead of Private Access with Private Inner Classes 考虑包内访问来取代访问私有内部类的私有修饰的方法或变量

为什么呢?下文的回答是:因为使用私有的内部类会产生静态的合成方法,影响性能


1. synthetic方法

如果有看过我的EventBus源码讲解的同学,会发现以下的一个常量 。

private static final int MODIFIERS_IGNORE = Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC;

/**
 *  省略内容
 *
 */ 

//判断是否为PUBLIC,MODIFIERS_IGNORE 包含(抽象,静态,桥接,合成,后2者是编译器添加的)
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) 

可以看到我在其上的注释 MODIFIERS_IGNORE 包含(抽象,静态,桥接,合成,后2者是编译器添加的)

我的注释写的没有错,后面两种方法是编译器添加的。在此我给大家来一个小demo,让看看合成方法是在怎么生成的:

public class OutClass {

    private static class InnerClass {

        public InnerClass(){}

        private int x;

        private  void y() {
        };
    }

    public static void main(String[] args) {
        InnerClass inner = new InnerClass();    

        inner.x = 2;

        System.out.println(inner.x);

        inner.y();

        for (Method m : InnerClass.class.getDeclaredMethods()) {
        System.out.println(String.format("%08X", m.getModifiers()) + " "
                    + m.getName());
        }

    }

简单的一个静态内部类,实列化之后通过反射遍历其所有方法。输出方法名的同时,也将所有方法修饰词的十六进制码的格式输出。
运行结果如下:

这里写图片描述

先不看前面的十六进制码,我们看看后面输出的方法名,看上面的代码,我们知道,我们只写了一个名为 y 的方法。那么前面的两个access$1,2,3 是哪里来鬼怪?

为了解开谜底,我们再继续看看 java.lang.reflect.Modifier 里的几个修饰词的十六进制码:

00001008 SYNTHETIC|STATIC
00000002 PRIVATE
00000111 NATIVE|FINAL|PUBLIC
00000011 FINAL|PUBLIC
00000001 PUBLIC
00001000 SYNTHETIC
00000008 STATIC

我们看到了 00001008 SYNTHETIC|STATIC 合成的静态方法修饰词

所以access$1什么的都是我们编译器合成的方法,且是静态的。


然后问题来了

为什么呢!(ಠ౪ಠ) 为什么生成这些方法?平时我反射其他没有内部类的class的时候没有出现过的啊….

真相

JVM是如何处理这个的class的?它可不知道什么是内部类或者嵌套类的。JVM对所有的类都一视同仁,它都认为是顶级类。被Javac编译后所有类都会被编译成顶级类,而那些内部类编译完后会生成…$… class的类文件。注意,是所有的内部类,我写静态内部类是因为这样的demo简单,所以如果你创建一个内部类的话,它会被彻底编译成一个顶级类。(原来JVM也是也挺笨的…..)。

既然同时身为顶级类,你OutClass 为什么可以直接调用我的InnerClass的私有方法和私有属性!你算老几?

为了解决这种矛盾,我们的圣母玛丽亚式的Javac编译的时候想到了一个解决冲突办法,在从中做了点手脚,在InnerClass里,为所有私有方法,和私有变量又写了一个合成的静态方法。为了还原真相我写了这样的代码:

private static class InnerClass {

        public InnerClass(){}

        private int x;

        synthetic static void access$1(int x) { this.x=x; }
        synthetic static int access$2(int x) { return x }

        private void y() {
        };
        synthetic static int access$3() { y() }
    }

所有说,等于是为了获取我们的x写了getter/setter方法。为了调用 y()写了一个嵌套的调用方法….
真是用心良苦啊…..

看完了上文,我们知道了,原来内部类在编译的时候为了给外部类调用,提供了几个静态方法给外部类使用。


补充点:(很重要啊)

1. 如果将y方法的修饰词改为public,就不会出现access$3()合成方法,对x也是同理。;

2.当你对x只赋值,不调用时,或者只调用,不赋值。只会产生相应的getter或setter方法

3.将构造函数变为private 也会生成一个非静态的合成方法,如下:

synthetic  void  OutClass$InnerClass(OuClass this$0) {  }

等下…this$0 这个又是什么鬼? 别急…我先下文介绍


2. synthetic对象

作为一个Android Coder ,有一点我们都知道:

内部类会持有外部类的指针,有可能发生内存泄漏。所以,我们常常建议在合适的时候,将内部类改为静态内部类,这样就可以不持有外部的指针。

可是,Do you know the reasons ?

我们还是上文的Class,不过将其简单的改造下,不过是将其输出隔离出来

public class OutClass {

    class InnerClass {
        public int x;
        public InnerClass() {}
        public  void y() {};
    }
}
public class Demo {

    /**
     * @param args
     */
    public static void main(String[] args) {
        //遍历所有字段
        for (Field  c  : OutClass.InnerClass.class.getDeclaredFields()) {
            System.out.println(String.format("%08X",  c.getModifiers()) + " "
                        + c.getName());
            }
        System.out.println("-----------------------------------------");
         //遍历构造函数 
         for( Constructor<?> c : OutClass.InnerClass.class.getDeclaredConstructors() ){
             System.out.println(String.format("%08X", 
                     c.getModifiers()) + " " + c.getName()+" ");
            //遍历构造函数入参
             for(Class temp:c.getParameterTypes()){
                 System.out.println(temp.getSimpleName());
             }
         }
    }
}

运行~,以下为结果~

这里写图片描述

我们可以看到,我们内部的字段有两个,一个是x, 一个是 this$0 。而我们的构造函数只有一个没错,可是却偷偷的传入了一个OutClass对象。
(/= _ =)/~┴┴
在javac 的编译之下…被改的东西还真多啊….为了让大家更直观。我再模拟下编译后的class 文件内的情况。

public class OutClass {

    class InnerClass {
        public int x;

        synthetic OutClass this$0;

        public InnerClass(OutClass args) {

            this.this$0 =args;
        }
        public  void y() {};
    }
}

这样就明白了吧~,我们的内部类之所以可以被外部类调用。原来是传了一个外部类的一个对象!有了this$0,我们可以随意的调用外部类的方法。

看完了上文,我们知道了,原来内部类在编译的时候为了调用外部类的方法,在其构造函数里传入了一个外部类的对象。

再接下上文,Android中为什么要将内部类改为静态内部类。是因为静态内部类是共享给当前类的所有对象的,所以不需要传入当前类的引用


补充点:(很重要啊)

1.如果内部类没有构造函数,就能避免传参么?NO!
这里写图片描述
即使是默认的构造函数,也会传外部类引用。 000000不在修饰词内,可以当作没有修饰词的构造方法。


总结

归根结底,作为一个Android的coder。那么多的static方法不适合移动的不算充裕的内存。因此Google不提倡使用私有内部类,最好将其拆开独立成一个Class。还有内部类尽量改为静态内部类,可以避免内存泄漏。有空看看google 的性能建议文档还是挺不错的!XD~~