java中的合成方法

2,090 阅读4分钟
反编译java代码时会经常看到Synthetic关键字,Synthetic修饰方法或者变量起到了什么作用呢?

Synthetic中文字面意思是人造的、合成的。Synthetic修饰方法或者变量就是标识该代码由编译器生成,Synthetic实际上是java字节码的访问标识access_flags中的一位。 access_flags总共占用2个字节,ACC_SYNTHETIC的值及含义如下:

标志名称 含义
ACC_SYNTHETIC 0×1000 synthetic,由编译器产生,不存在于源代码中。

另外java.lang.reflect.Method#isSynthetic的值就是来自此标志位。

那么在什么情况下会生成synthetic方法呢?synthetic方法对性能会有影响吗?

在一个java类中,可以访问自己的私有成员(滑稽.jpg),比如:

class Clazz1{
    private int mValue;
    
    public int run(){
        return mValue;
    }

    public static int run1(Clazz1 clazz1){
        return clazz1.mValue;
    }
}

根据java的封装特性,Clazz2中是无法访问Clazz1的private成员的。

public class Clazz1 {
    private int mValue;
}

class Clazz2 {
    public void run(){
        Clazz1 class1 = new Clazz1();
        int value = class1.mValue;//1 'mValue' has private access in 'Clazz1'
    }
    
    public static void run1(Clazz1 class1){
        int mValue = class1.mValue;//2 'mValue' has private access in 'Clazz1'
    }
}

但是对于内部类就完全不一样了

public class Foo {
    private int mValue;

    public void run() {
        Inner in = new Inner();
        mValue = 666;
        in.stuff();
    }
    
    private void print(int value) {
        System.out.println("Value is " + value);
    }

    private class Inner {
        void stuff() {
            Foo.this.print(Foo.this.mValue);
        }
    }
}

对于上述代码,定义了一个私有内部类(Foo$Inner),它会直接访问外部类中的私有方法和私有实例字段。这是合乎语法规则的,代码会按预期输出“Value is 666”。

java编译器为Foo$Inner.java生成的类的构造方法字节码中偷偷增加了外部类的引用作为参数,并且增加了一个成员属性保存了该引用。这就是我们能在内部类中直接访问外部类成员的实现原理。如下:

private class Inner {
    Foo this$0;
    public Inner(Foo foo){
        super();
        this.this$0 = foo;
    }
}

这就也是为什么内部类会持有外部类的引用的原因。

还有一个疑问,那么为什么内部类可以访问外部类的私有成员呢?

对于jvm来说 Foo$Inner就是一个普通的类,java虚拟机会认为从Foo$Inner直接访问Foo的私有成员不符合规则,因为Foo和Foo$Inner属于不同的类。

但是Java语言却允许内部类访问外部类的私有成员。(这就尴尬了)

那么这种访问到底是怎么做到的呢?为了支持这种访问,编译器会在Foo类中生成一些合成方法:

    static int access$100(Foo foo) {
        return foo.mValue;
    }
     static void access$200(Foo foo, int value) {
        foo.print(value);
    }

编译后再翻译回java源文件的完整代码如下:

public class Foo {
    private int mValue;

    public void run() {
        Inner in = new Inner(this);
        mValue = 27;
        in.stuff();
    }

    private void print(int value) {
        System.out.println("Value is " + value);
    }

    static int access$100(Foo foo) {
        return foo.mValue;
    }
    static void access$200(Foo foo, int value) {
        foo.print(value);
    }
    
    private class Inner {
        Foo this$0;
        public Inner(Foo foo){
            super();
            this.this$0 = foo;
        }
        void stuff() {
            //Foo.this.print(Foo.this.mValue);
            Foo.access$200(this$0,Foo.access$100(this$0));
        }
    }
}

因此,如果一个类A的私有成员a被内部类B访问,java编译器就会在类A中为a生成一个静态方法把a暴露出去。对私有成员a的访问也会替换成为A生成的方法的调用。当然私有方法同理。

在android开发中内部类的使用非常多,比如给view设置点击事件,Handler等等。示例代码如下:

public class SyntheticActivity extends Activity {
    private TextView textView;

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_synthetic);
        textView = findViewById(R.id.textView);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println(v.getClass().getSimpleName());
            }
        });
    }
}

编译器为OnClickListener生成的匿名内部类为SyntheticActivity$2,具体如下,可以看到确实为SyntheticActivity$2生成了带外部类引用的构造方法

.class LSyntheticActivity$2;
.super Ljava/lang/Object;
.source "SyntheticActivity.java"
.implements Landroid/view/View$OnClickListener;
.annotation system Ldalvik/annotation/EnclosingMethod;
    value = LSyntheticActivity;->onCreate(Landroid/os/Bundle;)V
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = null
.end annotation
.field final synthetic this$0:LSyntheticActivity;
.method constructor <init>(LSyntheticActivity;)V
    .registers 2
    .param p1, "this$0"    # LSyntheticActivity;
    iput-object p1, p0, LSyntheticActivity$2;->this$0:LSyntheticActivity;
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    return-void
.end method
.method public onClick(Landroid/view/View;)V
    ...省略...
.end method

编译器为Handler生成的匿名内部类为SyntheticActivity$1,具体如下,可以看到确实为SyntheticActivity$1生成了带外部类引用的构造方法。

.field final synthetic this$0:LSyntheticActivity;
.method constructor <init>(LSyntheticActivity;)V
    .registers 2
    .param p1, "this$0"    # LSyntheticActivity;
    iput-object p1, p0, LSyntheticActivity$1;->this$0:LSyntheticActivity;
    invoke-direct {p0}, Landroid/os/Handler;-><init>()V
    return-void
.end method
.method public handleMessage(Landroid/os/Message;)V
    .registers 2
    .param p1, "msg"    # Landroid/os/Message;
    invoke-super {p0, p1}, Landroid/os/Handler;->handleMessage(Landroid/os/Message;)V
    return-void
.end method

可以看到Handler确实持有了activity的引用,所以这里要注意内存泄漏(Looper - MessageQueue - message – handler – acitivity)。

除了内部类会生成合成方法之外,还有范型类中的桥接方法。范型是jdk5引入的特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 由于java的范型实现采用了类型擦出机制,泛型接口或类中,所有使用泛型形参的地方,全部擦除,替换为Object类型。

public class Generics<T>{
    public T get(T a){
        return a;
    }
    
    class Child extends Generics<String> { 
        @Override
        public String get(String a){
            System.out.println("Child");
            return a;
        }
    }
}

Generics范型擦除后如下:

public class Generics{
    public Object get(Object a){
        return a;
    }
}

测试代码

public static void main(String[] args) {
        Generics generics = new Child();
        String generics_test = (String) generics.get("Generics test");
        System.out.println(generics_test);
}
执行结果
Child
Generics test

上述代码中,Child类的get方法为什么可以被Override注解标注呢?这两个方法的签名不一致啊??不符合java继承及多态特性啊???测试代码为什么可以打出“Child”?

这是因为java在编译期间加入了桥接方法。编译后再翻译回java源文件,应如下所示:

public class Generics{
    public Object get(Object a){
        return a;
    }
    
    class Child extends Generics { 
        @Override
        //1
        public String get(String a){
            System.out.println("Child");
            return a;
        }
        
        //2
        public Object get(Object a){
            return get((String)a);
        }
    }
}

注意注释2处的代码只能通过编译器生成,如果你尝试自己写上注释2处的方法 IDE会提示:

'get(T)' in 'Generics' clashes with 'get(Object)' in 'Generics.Child'; 
both methods have same erasure,yet neither overrides the other

所以你不能自己加上该方法,对于开发者而言,Child的Object get(Object a)方法不可见的。

String generics_test = (String) generics.get("Generics test")调用的确实是Child类的Object get(Object a)方法,只不过通过他再去调用String get(String a)。

总结:外部类需要访问内部类的私有成员,或者内部类需要访问外部类的私有成员 ,或者匿名内部类中及范型类的子类中会生成一些合成方法。此外还存在合成类,暂不在本篇讨论范围。另外在android开发中推荐将内部类访问的字段和方法声明为拥有包访问权限(而非私有访问权限),从而避免产生相关开销。但这意味着同一包中的其他类可以直接访问这些字段,这破坏了java封装性,需要开发者自己做好权衡。