语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可不会被虚拟机接受,因此编译器为了让这些含有语法糖的代码正常工作其实需要对这些代码进行加工,经过编译器在生成class字节码的阶段完成解语法糖(desugar)的过程,那么这些语法糖最终究竟被编译成了什么呢,在这里列举了如下的一些Java典型的语法糖,结合实例和它们的编译结果分析一下。本文为该系列的第一篇。
泛型和类型擦除
java的泛型实际上是伪泛型,在编译后编译器会擦除泛型对象的参数化类型,也就是说源代码中的<T>类型
其实都会擦除,最终成为class字节码中的Object类型
,赋值等操作也就会直接转换为强制的类型转换,这样做无风险的原因是在编译的标注检查阶段其实已经进行了泛型的检查,如果当时无法通过检查的话编译无法通过。
另外,这个泛型信息不是真的就此丢掉了,class字节码中还是会保留Signature
属性来记录泛型对象在源码中的参数化类型。
代码:
public class Main {
public static void main(String[] args) {
List<String> strList = 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."<init>":()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<Ljava/lang/String;>;
其中的LocalVariableTypeTable
属性记录了strList
的擦除泛型前的类型:Ljava/util/List<Ljava/lang/String;>;
,翻译过来其实就是List<String>
,如果在反射中获取泛型变量的类型元信息,其来源其实就是这个Signature。这也算是Java为了弥补因类型擦除而导致的class字节码中的类型数据缺失而做出的额外努力吧。
变长参数:编译后变成数组类型的参数
变长参数会被编译成为数组类型的参数,变长参数只能出现在参数列表的结尾以消除歧义。
代码:
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) {
List<String> strList = 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."<init>":()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<String> 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."<init>":()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;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。