近期在看阿里的Android热修复技术原理一书中,看到Java的某些语法实现机制时,感觉获益匪浅,故这里记录一下,也是对Java语言的一些理解。
内部类的编译
我们都知道内部类会隐式持有外部类的引用,而静态内部类不是持有外部类的引用。其原理时Java编译器在编译时会把静态类编译成外部类一样的顶级类。非静态内部类,在编译完成后,在字节码文件中会生成一个外部类类型的this$0的引用。我们知道类的私有变量和方法,其他类是无法访问到的。既然都是外部类,那么内部类访问外部类的私有变量和私有方法是怎么实现的呢?答案是在编译期间自动为外部类生成了access$**的方法来封装外部类的私有成员和方法已供内部类使用。
下面的代码块解释了上文所说的内容:
public class OutClass {
private String a="abc";
private static String b="def";
class InnerClass{
private void print(){
System.out.println(a);
}
}
static class InnerStaticClass{
private void outprint(){
System.out.println(b);
}
}
}
使用javap查看编译后的字节码:
Compiled from "OutClass.java"
public class com.thunder.runoob.OutClass {
private java.lang.String a;
private static java.lang.String b;
public com.thunder.runoob.OutClass();
static java.lang.String access$000(com.thunder.runoob.OutClass);
static java.lang.String access$100();
static {};
}
Compiled from "OutClass.java"
class com.thunder.runoob.OutClass$InnerClass {
final com.thunder.runoob.OutClass this$0;
com.thunder.runoob.OutClass$InnerClass(com.thunder.runoob.OutClass);
private void print();
}
Compiled from "OutClass.java"
class com.thunder.runoob.OutClass$InnerStaticClass {
com.thunder.runoob.OutClass$InnerStaticClass();
private void outprint();
}
匿名内部类和非匿名内部类相比,匿名内部类是没有名字的,在编译期间,Java编译器会根据匿名内部类在外部类出现的顺序,生成命名为外部类&number的类。number会依次累加。
public class SourceTest {
public void $() {
new Runnable(){
@Override
public void run() {
System.out.println("hahaha!");
}
}.run();
}
public static void main(String[] arg) throws InterruptedException {
}
}
编译上面的Java代码,会生成2个class文件,分别是SourceTest.class,SourceTest$1.class;
编译后的class内容如下:
SourceTest$1.class
Compiled from "SourceTest.java"
class com.thunder.runoob.SourceTest$1 implements java.lang.Runnable {
final com.thunder.runoob.SourceTest this$0;
com.thunder.runoob.SourceTest$1(com.thunder.runoob.SourceTest);
public void run();
}
SourceTest.class
Compiled from "SourceTest.java"
public class com.thunder.runoob.SourceTest {
public com.thunder.runoob.SourceTest();
public void $();
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
}
<clinit>与<init>
在Java中,这俩个方法并不是由Java开发者定义的,而是由Java编译器自定生成的。当Java类中存在用static修饰的静态类型字段,或者存在使用static{}块包裹的逻辑时,编译器会自动生成<clinit>方法,有些人也将该方法称为类构造器。而当Java类定义了构造函数,或者其他非static类成员变量被赋予了初始值时,编译器会自动生成<init>方法。
在JVM运行过程中,如果遇到诸如new等指令时,会调用<init>方法。而当Java类中存在static变量,或者存在static{}包裹的代码块时,则JVM加载Java类时,会触发<clinit>方法,完成static变量的初始化,或者执行被static{}包裹的代码段的逻辑。
当开发者为Java类定义了非默认的构造函数时,Java编译器会修改构造函数的逻辑,将Java类成员的初始化逻辑所对应的字节码指令插入到构造函数所对应的字节码指令中。
关于Java类的<init>方法的生成规则,可以总结为以下几点:
1,无论一个Java类有无定义的构造函数,编译器都会自动生成一个默认的构造函数<init>()。
2,<init>()方法主要完成Java类的成员变量的初始化逻辑,同时会执行Java类中被{}包裹的块逻辑。当Java类中的成员变量没有被赋初值,则不会在<init>()方法中进行初始化。
3,如果显式的为Java类定义了多个构造函数,无论什么类型的构造函数。Java编译器都会将Java类成员变量的初始化逻辑嵌入到每一个构造函数中,并在构造函数自身逻辑之前。
4,当Java类显式继承了父类时,则Java编译器会让子类的各个构造函数调用父类的默认构造函数<init>(),从而完成父类成员变量的初始化逻辑。
5,当父类中定义了多个构造函数时,子类构造函数会调用父类默认构造函数。
6,子类构造函数调用父类默认构造函数的顺序,位于子类各个构造函数自身逻辑之前。
总结起来,一个类的各个构造函数执行顺序:调用父类的默认构造函数->执行类成员变量初始化逻辑和被{}包裹的块逻辑->执行构造函数自身逻辑。
泛型编译
泛型是java5才引入的,Java中的泛型基本上完全是在编译器中实现的。由编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码,虚拟机完全无感知泛型的存在。这就是我们常说的泛型擦除。编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除。
在java5以前要实现泛型的功能,只有用Object来实现,但是使用Object就会出现ClassCastException的情况,由于强制类型转换在语法上是可以的,所以编译器检查不出错误,因而在执行期就会发生ClassCastException,在java5之后,提出了针对泛型设计的解决方案。改方案在编译时进行类型安全检测,允许程序员在编译时就能检测到非法的类型。
public class GenericFoo<T> {
public T getFoo() {
return foo;
}
public void setFoo(T foo) {
this.foo = foo;
}
private T foo;
public static void main(String[] args){
GenericFoo<Boolean> genericFoo=new GenericFoo<>();
genericFoo.setFoo(new Boolean(true));
Boolean b=genericFoo.getFoo();
String str=(String) genericFoo.getFoo();// incovertiable types
}
}
上面这段代码,编译不会通过,此时泛型的优势就体现出来了,编译期间就检查到了可能的异常。
实际上,编译器在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器编译的时候去掉,这个过程就是类型擦除。上面的代码编译成class字节码后如下:
Compiled from "GenericFoo.java"
public class com.thunder.runoob.GenericFoo<T> {
public com.thunder.runoob.GenericFoo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public T getFoo();
Code:
0: aload_0
1: getfield #2 // Field foo:Ljava/lang/Object;
4: areturn
public void setFoo(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field foo:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class com/thunder/runoob/GenericFoo
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: new #5 // class java/lang/Boolean
12: dup
13: iconst_1
14: invokespecial #6 // Method java/lang/Boolean."<init>":(Z)V
17: invokevirtual #7 // Method setFoo:(Ljava/lang/Object;)V
20: aload_1
21: invokevirtual #8 // Method getFoo:()Ljava/lang/Object;
24: checkcast #5 // class java/lang/Boolean
27: astore_2
28: return
}
可以看到在编译之后泛型类型被Object替代了。如果此时再定义一个setFoo(Object foo)方法,编译器会报重复方法定义。但是如果限定了泛型的下确界,编译后就会被下确界类型替代。
在上面的代码我们可以看到,泛型是不需要强制类型转换的,实际上是在编译期间,编译器发现如果有一个变量的声明加上了泛型类型的话,编译器会自动加上check-cast类型转换,而不需要开发者手动进行强制类型转换。
泛型类型擦除与多态的冲突和解决
public class A<T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
class B extends A<Number> {
private Number number;
@Override
public Number get() {
return number;
}
@Override
public void set(Number number) {
this.number = number;
}
}
class C extends A {
private Number number;
@Override
public Number get() {
return number;
}
//@Override
public void set(Number o) {
this.number = o;
}
}
按照上面类型擦除的分析,类B的set和get方法是不可以被Override的,因为set(T t)在字节码中实际上是set(Object t),而类B方法set(Number n)方法参数不一样,此时类B的set方法理论上是重载而不是重写。这样,类型擦除就和多态产生了冲突。但是实际上JVM采用了一个特殊的方法来完成重写功能,就是bridge方法。可以看下B类的字节码
{
com.thunder.runoob.B();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/thunder/runoob/A."<init>":()V
4: return
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/thunder/runoob/B;
public java.lang.Number get();
descriptor: ()Ljava/lang/Number;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field number:Ljava/lang/Number;
4: areturn
LineNumberTable:
line 23: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/thunder/runoob/B;
public void set(java.lang.Number);
descriptor: (Ljava/lang/Number;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field number:Ljava/lang/Number;
5: return
LineNumberTable:
line 28: 0
line 29: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/thunder/runoob/B;
0 6 1 number Ljava/lang/Number;
public void set(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/Number
5: invokevirtual #4 // Method set:(Ljava/lang/Number;)V
8: return
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/thunder/runoob/B;
public java.lang.Object get();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #5 // Method get:()Ljava/lang/Number;
4: areturn
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/thunder/runoob/B;
}
可以看到编译器给我们自动合成了set(Ljava/lang/Object)和get()Ljava/lang/Object;通过这俩个bridge方法来重写父类方法,同时这两个bridge方法实际上调用set(Ljava/lang/Number;)和get()Ljava/lang/Number;这两个重载方法。JVM巧妙使用了桥方法方式,来解决了类型擦除和多态的冲突。