编译期语法糖(一)

编译期语法糖(一)

所谓的语法糖,就是指java编译器在把.java文件编译成.class字节码的过程中,自动生成和转换的一些代码.主要是为了减轻程序猿的负担.

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

默认构造器

我们先创建一个没有默认构造器的类

1
2
public class Demo04_1 {
}

使用javap命令反编译,得到如下字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
public demo04.Demo04_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo04/Demo04_1;
}

可以看出,编译期自动生成了一个无参的构造器,引用了父类,也就是Object的init 方法.

就相当于下面代码:

1
2
3
4
5
public class Demo04_1 {
public Demo04_1(){
super();
}
}

自动拆装箱

在Java SE5中,为了减少开发人员的工作,Java提供了自动拆箱与自动装箱功能。

自动装箱: 就是将基本数据类型自动转换成对应的包装类。

自动拆箱:就是将包装类自动转换成对应的基本数据类型。

例如下面代码:

1
2
3
4
5
6
public class Demo04_2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}

我们同样适用javap反编译,然后查看main方法中的code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(java.lang.String[]);
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
LineNumberTable:
...
LocalVariableTable:
...

1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

查看字节码,可以看出是同步Integer.valueOf()进行装箱

6: invokevirtual #3 // Method java/lang/Integer.intValue:()I

通过Integer.inValue()进行拆箱


将字节码转换为java代码如下:

1
2
3
4
5
6
public class Demo04_2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}

泛型擦除

在Java字节码中是没有泛型这一概念, 只有普通方法和普通类, 所有泛型类的泛型参数都会在编译时期被擦除, 所以泛型类并没有自己独有的Class类对象比如List.class, 而只有List.class对象。

我们先来看一段代码:

1
2
3
4
5
6
7
public class Demo04_3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Integer a = list.get(0);
}
}

同样进行反编译,然后查看main方法的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return

22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;

27: checkcast #7 // class java/lang/Integer

通过这两行可以看出,调用list.get()返回结果为Object.

然后通过类型转换指令 checkcast 转换成Integer对象.

相当于:

1
Integer a = (Integer) list.get(0);

虽然在编译期泛型在code代码中擦除,但是我们却可以在字节码的其他地方获得这些信息

1
2
3
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;

这就是一个泛型信息表,表示槽位1中的list的泛型为Ljava/lang/Integer.


泛型擦除可能引起的问题

我们来看下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo04_3 {
public static void main(String[] args) {
Map<String, Object> map = new HashMap<String, Object>();
List<Integer> list1 = Arrays.asList(1,2,3);
map.put("list",list1);

List<String> list2 = (List<String>)map.get("list");
for(String str : list2){
System.out.println(str);
}
}
}

这是,在编译期是不报错的,好想是正确执行下面的类型转换

List<String> list2 = (List<String>)map.get("list");

但是在运行时,却会报出

1
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

这是由于,在编译期,进行了泛型擦除,而再get后进行类型转换时,jvm也没有(List)的概念,也当成是List类型来转换,所以转换时成功的,但是在list中,其实还是Integer类型的元素.所以当成字符串来遍历时,自然会报错.

可变参数

先看下面一段代码:

1
2
3
4
5
6
7
8
9
10
public class Demo04_4 {
public static void foo(String... args){
String[] arr = args;
System.out.println(arr);
}

public static void main(String[] args) {
foo("Hello","World");
}
}

通过Javap反编译

我们先看foo方法的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void foo(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: astore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: aload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
9: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
2 8 1 arr [Ljava/lang/String;
([Ljava/lang/String;)V```
1
2
3
4
5
6
7
8
9
10

从参数描述中,可以看出 入参是一个String[]

0: aload_0
1: astore_1
从这也可以看出,转换成String[] arr时,没有进行类型转换,进一步证明了可变参数,其实就是数组



然后我们在看main方法的字节码:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: iconst_2
1: anewarray #4 // class java/lang/String
4: dup
5: iconst_0
6: ldc #5 // String Hello
8: aastore
9: dup
10: iconst_1
11: ldc #6 // String World
13: aastore
14: invokestatic #7 // Method foo:([Ljava/lang/String;)V
17: return

1
2
3

```java
1: anewarray #4 // class java/lang/String

可以看出是新new了一个数组,当做可变参数传给了foo

也就是,编译器将上述代码转换为

1
2
3
4
5
6
7
8
9
10
public class Demo04_4 {
public static void foo(String[] args){
String[] arr = args;
System.out.println(arr);
}

public static void main(String[] args) {
foo(new String[]{"Hello","World"});
}
}

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

0%