摘要:操作对应字节码中的个字节我们可以看到最关键的操作其实就是调用的其实是类的方法,此方法的入参类型是,返回值类型是,翻译过来就是类的方法,执行完后将获得的结果做了,检查返回的对象类型是否是。
语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可不会被虚拟机接受,因此编译器为了让这些含有语法糖的代码正常工作其实需要对这些代码进行加工,经过编译器在生成class字节码的阶段完成解语法糖(desugar)的过程,那么这些语法糖最终究竟被编译成了什么呢,在这里列举了如下的一些Java典型的语法糖,结合实例和它们的编译结果分析一下。本文为该系列的第一篇。
泛型和类型擦除java的泛型实际上是伪泛型,在编译后编译器会擦除泛型对象的参数化类型,也就是说源代码中的
另外,这个泛型信息不是真的就此丢掉了,class字节码中还是会保留Signature属性来记录泛型对象在源码中的参数化类型。
代码:
public class Main { public static void main(String[] args) { ListstrList = new ArrayList<>(); strList.add("aaa"); String strEle = strList.get(0); } }
main方法在javap编译后的字节码
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."":()V 7: astore_1 8: aload_1 9: ldc #4 // String aaa 11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 16: pop 17: aload_1 18: iconst_0 19: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 24: checkcast #7 // class java/lang/String 27: astore_2 28: return
上面我们演示了一个参数化类型为String的List的泛型对象strList的add和get操作:
add操作:对应字节码中的8~16个字节:我们可以看到最关键的add操作其实就是
invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
调用的其实是java/util/List类的add方法,此方法的入参类型是Ljava/lang/Object;,返回值类型是Z,翻译过来就是List类的boolean add(Object o)方法,这里并没有参数化类型String的什么事情。
get操作:对应字节码中的17~27个字节:我们可以看到最关键的get操作其实就是
invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; checkcast #7 // class java/lang/String
调用的其实是java/util/List类的get方法,此方法的入参类型是I,返回值类型是Ljava/lang/Object;,翻译过来就是List类的Object get(int i)方法,执行完后将获得的结果做了checkcast,检查返回的对象类型是否是String。
从上面的分析我们不难看出,Java泛型到了编译出结果的时候参数化类型已经没有什么作用了,就是简单做了强制的类型转换。这段去掉了语法糖的代码如下:
public class Main { public static void main(String[] args) { List strList = new ArrayList(); strList.add((Object)"aaa"); String strEle = (String) strList.get(0); } }
Java的泛型是伪泛型的原因如上,在运行时这个代码完全体会不到不同参数化类型的List有什么不同。而泛型参数化类型的用武之地更多的是在编译时用来做检验类型使用的,正常情况下如果编译时通过检验当然就不会在运行期类型强制转换的时候出现异常,更何况其实字节码中还有checkcast的显式类型检查。
如果使用javac的-g:vars参数来保留class字节码中方法的局部变量信息,那么我们可以看到额外的信息:
LocalVariableTable: Start Length Slot Name Signature 0 29 0 args [Ljava/lang/String; 8 21 1 strList Ljava/util/List; 28 1 2 strEle Ljava/lang/String; LocalVariableTypeTable: Start Length Slot Name Signature 8 21 1 strList Ljava/util/List;
其中的LocalVariableTypeTable属性记录了strList的擦除泛型前的类型:Ljava/util/List
变长参数会被编译成为数组类型的参数,变长参数只能出现在参数列表的结尾以消除歧义。
代码:
public class Main { public static void method(String... args) { } }
method方法在编译后:
public static void method(java.lang.String...); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS Code: stack=0, locals=1, args_size=1 0: return
我们可以清楚地看到方法的特征符是([Ljava/lang/String;)V,即参数是[Ljava/lang/String;,翻译过来就是String[],即数组类型。
这段去掉了语法糖的代码如下:
public class Main { public static void method(String[] args) { } }自动装箱拆箱
编译后装箱通过valueOf()变成了对象,拆箱通过xxxValue()变成了原始类型值。
代码:
public class Main { public static void main(String[] args) { Integer x = 1; int y = x; } }
main方法编译后:
descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_1 1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 4: astore_1 5: aload_1 6: invokevirtual #3 // Method java/lang/Integer.intValue:()I 9: istore_2 10: return
这里我们可以明显看到Integer x = 1;编译时x转换成了java/lang/Integer.valueOf生成的引用类型Integer变量,而int y = x;编译时y转换成了java/lang/Integer.intValue生成的原始类型int变量。
去掉了语法糖的代码如下:
public class Main { public static void main(String[] args) { Integer x = Integer.valueOf(1); int y = x.intValue(); } }遍历循环
编译后变成了迭代器遍历。
代码:
public class Main { public static void main(String[] args) { ListstrList = new ArrayList<>(); for (String str : strList) { System.out.println(str); } } }
main方法编译后:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."":()V 7: astore_1 8: aload_1 9: invokeinterface #4, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 14: astore_2 15: aload_2 16: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 21: ifeq 44 24: aload_2 25: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 30: checkcast #7 // class java/lang/String 33: astore_3 34: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 37: aload_3 38: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 41: goto 15 44: return StackMapTable: number_of_entries = 2 frame_type = 253 /* append */ offset_delta = 15 locals = [ class java/util/List, class java/util/Iterator ] frame_type = 250 /* chop */ offset_delta = 28
从上面我们可以看到遍历循环的语法糖被替换成了List.iterator的循环操作,用下面的代码即可表达这段编译后的去掉语法糖的代码:
public class Main { public static void main(String[] args) { List条件编译strList = new ArrayList<>(); Iterator strIterator = strList.iterator(); while(strIterator.hasNext()){ System.out.println((String) strIterator.next()); } } }
编译后将常量不可达条件分支直接在编译结果中消除掉。
代码:
public class Main { public static void main(String[] args) { if (true) { System.out.println("Yes"); } else { System.out.println("No"); } } }
main方法编译后:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Yes 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; }
从上面我们可以看到常量不可达条件直接就在编译结果中略去了,仿佛就没有这个分支一样,用下面的代码即可表达这段编译后的去掉语法糖的代码:
public class Main { public static void main(String[] args) { System.out.println("Yes"); } }
需要注意的是这里强调的是常量不可达条件才会略去,比如直接就是true的分支或者1==1这样的分支是会保留的,如果是变量经过运算后才被确定为不可达是不会发生这种条件编译的,比如:
public class Main { public static void main(String[] args) { int i = 1; if (i==1) { System.out.println("Yes"); } else { System.out.println("No"); } } }
编译后还是会走ifelse判断:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: iconst_1 1: istore_1 2: iload_1 3: iconst_1 4: if_icmpne 18 7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 10: ldc #3 // String Yes 12: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 15: goto 26 18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 21: ldc #5 // String No 23: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 26: return LocalVariableTable: Start Length Slot Name Signature 0 27 0 args [Ljava/lang/String; 2 25 1 i I StackMapTable: number_of_entries = 2 frame_type = 252 /* append */ offset_delta = 18 locals = [ int ] frame_type = 7 /* same */ }内部类
内部类即是类中类,我们来看这个简单的例子:
代码:
public class Main { class Person{ String name; Integer age; public Person(String name, Integer age) { this.name = name; this.age = age; } } public void demo(String[] args) { Person person = new Person("ccc", 20); } }
来看看编译后的结果,编译后会将内部类Person多带带拿出来做编译,不过语法糖褪去后编译器做了一些处理,比如为Person类加了与外部的Main类相联系的字段this$0:
... class top.jinhaoplus.Main$Person ... { java.lang.String name; descriptor: Ljava/lang/String; flags: java.lang.Integer age; descriptor: Ljava/lang/Integer; flags: final top.jinhaoplus.Main this$0; descriptor: Ltop/jinhaoplus/Main; flags: ACC_FINAL, ACC_SYNTHETIC public top.jinhaoplus.Main$Person(top.jinhaoplus.Main, java.lang.String, java.lang.Integer); descriptor: (Ltop/jinhaoplus/Main;Ljava/lang/String;Ljava/lang/Integer;)V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=4 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Ltop/jinhaoplus/Main; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."":()V 9: aload_0 10: aload_2 11: putfield #3 // Field name:Ljava/lang/String; 14: aload_0 15: aload_3 16: putfield #4 // Field age:Ljava/lang/Integer; 19: return LocalVariableTable: Start Length Slot Name Signature 0 20 0 this Ltop/jinhaoplus/Main$Person; 0 20 1 this$0 Ltop/jinhaoplus/Main; 0 20 2 name Ljava/lang/String; 0 20 3 age Ljava/lang/Integer; }
这里翻译过来类似这样的:
class Person { String name; Integer age; final Main this$0; public Person(final Main this$0, String name, Integer age) { this.this$0 = this$0; this.name = name; this.age = age; } } public class Main { public void demo(String[] args) { Person person = new Person(this, "ccc", 20); } }
至于为什么需要这个多余的外部类的字段呢,其实是为了通过它来获取外部类中的信息,我们对例子加以改造,添加两个外部类的字段secret1和secret2:
public class Main { private String secret1; private String secret2; class Person{ String name; Integer age; public Person(String name, Integer age) { this.name = name; this.age = age; } public void getSecrets(){ System.out.println(secret1); System.out.println(secret2); } } public void demo(String[] args) { Person person = new Person("ccc", 20); person.getSecrets(); } }
这个时候编译的结果是Main为了对外提供自己属性的值自动添加了静态方法access$000(Main)和access$100(Main):
static java.lang.String access$000(top.jinhaoplus.Main); descriptor: (Ltop/jinhaoplus/Main;)Ljava/lang/String; flags: ACC_STATIC, ACC_SYNTHETIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field secret1:Ljava/lang/String; 4: areturn LocalVariableTable: Start Length Slot Name Signature 0 5 0 x0 Ltop/jinhaoplus/Main; static java.lang.String access$100(top.jinhaoplus.Main); descriptor: (Ltop/jinhaoplus/Main;)Ljava/lang/String; flags: ACC_STATIC, ACC_SYNTHETIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #1 // Field secret2:Ljava/lang/String; 4: areturn LocalVariableTable: Start Length Slot Name Signature 0 5 0 x0 Ltop/jinhaoplus/Main; }
而内部类编译后的结果在获取外部类的属性的时候其实就是调用暴露出的这些方法:
public void getSecret(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:Ltop/jinhaoplus/Main; 7: invokestatic #6 // Method top/jinhaoplus/Main.access$000:(Ltop/jinhaoplus/Main;)Ljava/lang/String; 10: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 16: aload_0 17: getfield #1 // Field this$0:Ltop/jinhaoplus/Main; 20: invokestatic #8 // Method top/jinhaoplus/Main.access$100:(Ltop/jinhaoplus/Main;)Ljava/lang/String; 23: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 26: return LocalVariableTable: Start Length Slot Name Signature 0 27 0 this Ltop/jinhaoplus/Main$Person; }
翻译过来其实就是这样子的:
class Person { String name; Integer age; final Main this$0; public Person(final Main this$0, String name, Integer age) { this.this$0 = this$0; this.name = name; this.age = age; } public void getSecrets(){ System.out.println(Main.access$000(this$0)); System.out.println(Main.access$100(this$0)); } } public class Main { private String secret1; private String secret2; public void demo(String[] args) { Person person = new Person(this, "ccc", 20); } public static String access$000(Main main) { return main.secret1; } public static String access$100(Main main) { return main.secret2; } }
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/77050.html
摘要:因此,对应地我们可以翻译这段二进制字节码为这样的代码注意,这段代码并不能通过编译,因为源码这一层是不允许直接继承的,这个继承过程只允许在编译器内部解语法糖的过程中被编译器添加,添加之后的类才会有的访问标识符。 语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可...
摘要:但其实,虚拟机并不支持这些语法糖。方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。GitHub 2.5k Star 的Java工程师成神之路 ,不来了解一下吗); GitHub 2.5k Star 的Java工程师成神之路 ,真的不来了解一下吗); GitHub 2.5k Star 的Java工程师成神之路 ,真的确定不来了解一下吗); 本文从 ...
摘要:但其实,虚拟机并不支持这些语法糖。方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。GitHub 2.5k Star 的Java工程师成神之路 ,不来了解一下吗); GitHub 2.5k Star 的Java工程师成神之路 ,真的不来了解一下吗); GitHub 2.5k Star 的Java工程师成神之路 ,真的确定不来了解一下吗); 本文从 ...
摘要:但其实,虚拟机并不支持这些语法糖。方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。GitHub 2.5k Star 的Java工程师成神之路 ,不来了解一下吗); GitHub 2.5k Star 的Java工程师成神之路 ,真的不来了解一下吗); GitHub 2.5k Star 的Java工程师成神之路 ,真的确定不来了解一下吗); 本文从 ...
摘要:提供给了用户大量的语法糖,比如泛型自动装箱拆箱循环变长参数内部类枚举类断言新特性方法引用等解语法糖语法糖的存在主要是方便开发人员使用。 首先,部分总结文字引用 简书作者:Eric新之助 。链接:https://www.jianshu.com/p/4de08deb6ba4 已获得授权 showImg(https://segmentfault.com/img/bVbfuX9?w=646&...
阅读 639·2021-10-13 09:39
阅读 1449·2021-09-09 11:53
阅读 2637·2019-08-29 13:55
阅读 721·2019-08-28 18:08
阅读 2583·2019-08-26 13:54
阅读 2406·2019-08-26 11:44
阅读 1835·2019-08-26 11:41
阅读 3758·2019-08-26 10:15