0


【JVM】字节码技术:分析语法糖原理(前端编译优化)

一、概念

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃 嘛)

注意:以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

二、默认构造器

publicclassCandy1{}
publicclassorg.example.classLoading.Candy1...Constant pool:
    #1=Methodref          #3.#13// java/lang/Object."<init>":()V...{// 编译器优化:这个无参构造是编译器帮助我们加上的publicorg.example.classLoading.Candy1();...Code:
      stack=1, locals=1, args_size=10: aload_0
         1: invokespecial #1//super();4:returnLineNumberTable:...LocalVariableTable:StartLengthSlotNameSignature050thisLorg/example/classLoading/Candy1;}

二、自动拆装箱

代码片段1 :显然当前版本太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是 包装类型)

publicclassCandy2{publicstaticvoidmain(String[] args){Integer x =Integer.valueOf(1);int y = x.intValue();}}

因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2

代码片段2: 这段代码在 JDK 5 之前是无法编译通过的,JDK 5 开始自动优化

publicclassCandy2{publicstaticvoidmain(String[] args){Integer x =1;//编译器进行优化👆int y = x;//编译器进行优化👆}}

三、泛型擦除、反射泛型

泛型是在 JDK 5 开始加入的特性,主要是在编译期间做类型检查。
java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:取值时,字节码需做一个强制类型转换

publicclassCandy3{publicstaticvoidmain(String[] args){List<Integer> list=newArrayList<>();
        list.add(10);Integer x=list.get(0);//取值时,字节码需做一个强制类型转换}}

泛型是为了在编译期间的类型检查,泛型擦除是为了便于字节码处理

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable(局部变量类型表) 仍然保留了方法参数泛型的信息

publicstaticvoidmain(java.lang.String[]);
    descriptor:([Ljava/lang/String;)V
    flags:ACC_PUBLIC,ACC_STATICCode:
      stack=2, locals=3, args_size=10:new           #23: dup
         4: invokespecial #37: astore_1
         8: aload_1
         9: bipush        1011: invokestatic  #4// 1.Integer.valueOf(10)14: invokeinterface #5,2// 2.注意为Object:List.add(Object)19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,2// 3.list.get(Object);27: checkcast     #7// 4.注意强制类型转换:(Integer)list.get(0);30: astore_2
        31:returnLineNumberTable:...LocalVariableTable:StartLengthSlotNameSignature0320  args   [Ljava/lang/String;8241  list   Ljava/util/List;3112     x   Ljava/lang/Integer;LocalVariableTypeTable:StartLengthSlotNameSignature8241  list   Ljava/util/List<Ljava/lang/Integer;>;

反射泛型

注意:局部遍历的泛型信息无法通过反射获取。只有类结构上的泛型可以通过反射获取

publicclassCandy3{publicstaticvoidmain(String[] args)throwsNoSuchMethodException{Method test =Candy3.class.getMethod("test",List.class,Map.class);Type[] types = test.getGenericParameterTypes();for(Type type : types){if(type instanceofParameterizedType){ParameterizedType parameterizedType=(ParameterizedType) type;System.out.println("原始类型 - "+parameterizedType.getRawType());Type[] arguments = parameterizedType.getActualTypeArguments();for(int i=0;i<arguments.length;i++){System.out.printf("泛型参数[%d] - %s\n",i,arguments[i]);}}}}publicSet<Integer>test(List<String> list,Map<Integer,Object> map){returnnewHashSet<>();}}
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

四、可变参数

可变参数也是 JDK 5 开始加入的新特性:

publicclassCandy4{publicstaticvoidmain(String[] args){foo("hello","world");}publicstaticvoidfoo(String... args){String[] array = args;//可以直接赋值System.out.println(array);}}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。

同样 java 编译器会在编译期间将上述代码变换为:

注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去

publicclassCandy4{publicstaticvoidmain(String[] args){foo(newString[]{"hello","world"});//编译器进行优化}publicstaticvoidfoo(String[] args){String[] array = args;// 可以直接赋值System.out.println(array);}}

五、foreach循环

仍是 JDK 5 开始引入的语法糖。

注意: foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用。其中 Iterable 用来获取集合的迭代器( Iterator )

数组的循环

publicclassCandy5_1{publicstaticvoidmain(String[] args){int[] array ={1,2,3,4,5};for(int e : array){System.out.println(e);}}}

编译器编译后的字节码:

publicclassCandy5_1{publicstaticvoidmain(String[] args){int[] array =newint[]{1,2,3,4,5};//编译器进行优化for(int i =0; i < array.length;++i){//编译器进行优化int e = array[i];System.out.println(e);}}}

集合的循环

publicclassCandy5_2{publicstaticvoidmain(String[] args){List<Integer> list =Arrays.asList(1,2,3,4,5);for(Integer i : list){System.out.println(i);}}}

编译器编译后的字节码:迭代器的调用

publicclassCandy5_2{publicstaticvoidmain(String[] args){List<Integer> list =Arrays.asList(1,2,3,4,5);Iterator iter = list.iterator();//编译器进行优化while(iter.hasNext()){//编译器进行优化Integer e =(Integer)iter.next();System.out.println(e);}}}

六、switch 字符串

从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

注意:配合 String 和枚举使用时,不能 case “null”。因为 null 不能调用hashCode()

publicclassCandy6_1{publicstaticvoidchoose(String str){switch(str){case"hello":{System.out.println("h");break;}case"world":{System.out.println("w");break;}}}}

编译器编译后的字节码:将字符串转换为 byte 类型,再进行比较

publicclassCandy6_1{publicstaticvoidchoose(String str){byte x =-1;switch(str.hashCode()){case99162322://"hello".(hashCode):编译器进行优化if(str.equals("hello")){
                    x =0;}break;case113318802://"world".(hashCode):编译器进行优化if(str.equals("world")){
                    x =1;}}switch(x){case0:System.out.println("h");break;case1:System.out.println("w");}}}

七、switch 枚举

switch 枚举的例子,原始代码:

publicclassCandy7{publicstaticvoidfoo(Sex sex){switch(sex){caseMALE:System.out.println("男");break;caseFEMALE:System.out.println("女");break;}}}enumSex{MALE,FEMALE}

编译器编译后的字节码:

publicclassCandy7{//编译器进行优化:定义一个合成类,仅 jvm 使用staticclass $MAP{staticint[] map =newint[2];//大小即为枚举元素个数static{
            map[Sex.MALE.ordinal()]=1;//存储case对应的数字
            map[Sex.FEMALE.ordinal()]=2;}}publicstaticvoidfoo(Sex sex){switch(sex.ordinal()){//编译器进行优化case1:System.out.println("男");break;case2:System.out.println("女");break;}}}

八、枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

enumSex{MALE,FEMALE}

编译器编译后的字节码:

finalclassSexextendsEnum<Sex>{//编译器进行优化static{MALE=newSex("MALE",0);FEMALE=newSex("FEMALE",1);
        $VALUES=newSex[]{MALE,FEMALE};}publicstaticfinalSexMALE;publicstaticfinalSexFEMALE;privatestaticfinalSex[] $VALUES;privateSex(String name,int ordinal){super(name, ordinal);}publicstaticSex[]values(){return(Sex[])$VALUES.clone();}publicstaticSexvalueOf(String name){return(Sex)Enum.valueOf(Sex.class,name);}}

九、try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法

try-with-resources

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

publicclassCandy9{publicstaticvoidmain(String[] args){try(资源变量 = 创建资源对象){}catch(){}}}

编译器编译后的字节码:

publicclassCandy9{publicstaticvoidmain(String[] args){try{//编译器进行优化InputStream is =newFileInputStream("d:\\1.txt");Throwable t =null;try{System.out.println(is);}catch(Throwable e1){
                t = e1;throw e1;}finally{if(is !=null){if(t !=null){try{
                            is.close();}catch(Throwable e2){
                            t.addSuppressed(e2);//注意:输出外层套内存的异常}}else{
                        is.close();}}}}catch(IOException e){
            e.printStackTrace();}}}

十、方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
classA{publicNumberm(){return1;}}classBextendsA{@OverridepublicIntegerm(){//返回值类型与父类不一致,那怎么说是重写呢?return2;}}

对于子类,编译器编译后的字节码:

classBextendsA{publicIntegerm(){return2;}// 合成方法,仅JVM可见:真正的重写方法public synthetic bridge Numberm(){returnm();}}

验证

publicclassCandy10{publicstaticvoidmain(String[] args){Method[] declaredMethods =B.class.getDeclaredMethods();for(Method declaredMethod : declaredMethods){System.out.println(declaredMethod);//输出两个m()}}}
#两个m()
public java.lang.Integer org.example.classLoading.candy.B.m()
public java.lang.Number org.example.classLoading.candy.B.m()

十一、匿名内部类

源代码:

publicclassCandy11{publicstaticvoidmain(String[] args){Runnable runnable =newRunnable(){@Overridepublicvoidrun(){System.out.println("ok");}};}}

编译器编译后的字节码:

publicclassCandy11{publicstaticvoidmain(String[] args){Runnable runnable =newCandy11$1();}}// 额外生成的类finalclassCandy11$1implementsRunnable{Candy11$1(){}publicvoidrun(){System.out.println("ok");}}

引用 局部变量 的匿名内部类,源代码:

注意

为什么匿名内部类引用局部变量时,局部变量必须是 final 的?

  • 因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11 1 对 象 的 v a l 1 对象的 val 1对象的valx 属性,所以 x 不应该再发生变 化了。如果变化,那么 val$x 属性没有机会再跟着一起变化
publicclassCandy11{publicstaticvoidtest(finalint x){Runnable runnable =newRunnable(){@Overridepublicvoidrun(){System.out.println("ok:"+ x);}};}}

编译器编译后的字节码:

publicclassCandy11{publicstaticvoidtest(finalint x){Runnable runnable =newCandy11$1(x);}}// 额外生成的类finalclassCandy11$1implementsRunnable{int val$x;Candy11$1(int x){this.val$x = x;}publicvoidrun(){System.out.println("ok:"+this.val$x);}}
标签: jvm 前端 java

本文转载自: https://blog.csdn.net/weixin_43401592/article/details/128019546
版权归原作者 愿你满腹经纶 所有, 如有侵权,请联系我们删除。

“【JVM】字节码技术:分析语法糖原理(前端编译优化)”的评论:

还没有评论