反编译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封装性,需要开发者自己做好权衡。