请给我一颗语法糖

张天宇 on 2020-05-24

语法糖。

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

这里使用java代码来解释一些语法糖。

1 默认构造器

1
public class Candy{}

编译成class后的代码

1
2
3
4
5
6
public class Candy{
// 编译器将帮助我们加上无参构造,即调用父类Object的无参构造方法
public Candy(){
super();
}
}

2 自动拆装箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JDK 1.5之前
public class Candy{
public static void main(String[] args){
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
//之后
public class Candy{
public static void main(String[] args){
Integer x = 1;
int y = x;
}
}

3 泛型集合取值

泛型在JDK5之后加入的特性,但Java在编译泛型代码后会执行泛型擦除动作,即泛型信息在编译为字节码后就丢失了,实际的类型都当作了Object类型来处理。

1
2
3
4
5
6
7
public class Candy{
public static void main(String[] args){
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是List.add(Object e)
Integer x = list.get(0); // 实际调用的是Object obj = List.get(int index);
}
}

所以在取值时,编译器真正生成的字节码中,还需要额外做一个类型转换的动作:

1
2
// 需要将Object转换为Integer
Integer x = (Integer)list.get(0);

如果前面的x变量类型修改为int基本类型那么最终生成的字节码是:

1
2
// 需要将Object转换为Integer,并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

擦除的是字节码上的泛型信息,LocalVariabletypeTable局部变量类型泛型表仍然保留了方法参数泛型的信息。

4 可变参数

JDK5开始加入的新特性。

1
2
3
4
5
6
7
8
9
public class Candy{
public static void foo(String... args){
String[] array = args;
System.out.println(array);
}
public static void main(String[] args){
foo("hello", "world");
}
}

可变参数String… args其实是一个String[] args,从代码中的赋值语句中就可以看出来。Java编译器会在编译期间将上述代码变换为:

1
2
3
4
5
6
7
8
9
public class Candy{
public static void foo(String[] args){
String[] array = args;
System.out.println(array);
}
public static void main(Stringp[] args){
foo(new String[]{"hello","world"});
}
}

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

5 foreach循环

1
2
3
4
5
6
7
8
public class Candy{
public static void main(String[] args){
int[] array = {1, 2, 3, 4, 5};
for (int e : array){
System.out.println(e);
}
}
}

会被编译器转换为:

1
2
3
4
5
6
7
8
9
10
11
public class Candy{
public Candy(){
}
public static void main(String[] args){
int[] array = new int[]{1, 2, 3, 4, 5};
for (int i=0; i < array.length; ++i){
int e = array[i];
System.out.println(e);
}
}
}

集合的循环,

1
2
3
4
int[] array = {1, 2, 3, 4, 5};
for (int i : array){
System.out.println(i);
}

实际被编译器转化为对迭代器的引用:

1
2
3
4
5
6
7
8
9
10
11
public class Candy{
public Candy(){}
public static void main(String[] args){
List<Integer> list = Array.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while (iter.hasNext()){
Integer e = (Integer) iter.next();
System.out.println(e);
}
}
}

6 switch字符串

switch可以作用于字符串和枚举类,但变量不能为null。

1
2
3
4
5
6
7
8
9
10
switch (str){
case "hello":{
System.out.println("h");
break;
}
case "world":{
System.out.println("w");
break;
}
}

会被编译器转换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
switch(str.hashCode()){
case 99162322: // hello的hashCode,提高比较效率,减少比较次数
if (str.equals("hello")){ // equals防止hashcode冲突
x = 0;
}
break;
case 113318802: // world的hashCode
if (str.equals("world")){
x = 1;
}
}
// byte类型
switch (x){
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}

hashcode相同的情况:

1
2
3
4
5
6
7
8
9
10
switch (str){
case "BM":{
System.out.println("h");
break;
}
case "C.":{
System.out.println("w");
break;
}
}

将被编译器转换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch(str.hashCode()){
case 2123: //hashcode值相同,需要equals
if (str.equals("C.")){
x = 1;
} else if (str.equals("BM")){
x = 0;
}
default:
switch(x){
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}

7 switch枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Sex{
MALE, FEMALE
}
public class Candy{
public static void foo(Sex sex){
switch (sex){
case MALE:
System.out.println(“男”);
break;
case FEMALE:
System.out.println(“女”);
break
}
}
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Candy{
// 定义一个合成类,仅JVM使用,对我们不可见
// 用来映射枚举的ordinal与数组元素的关系
// 枚举的ordinal表示枚举对象的序号是从0开始的
// 即MALE的ordinal()=0,FEMALE的ordinal()=1
static class $MAP{
//数组大小即为枚举元素个数,里面存储case用来对比的数字
static int [] map = new int [2];
static{
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex){
int x = $MAP.map[sex.ordinal()];
switch (x){
case 1:
System.out.println(“男”);
break;
case 2:
System.out.println(“女”);
break
}
}
}

8 枚举类

1
2
3
enum Sex{
MALE, FEMALE
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class Sex extends Enum<Sex>{ // 枚举类不能再被继承
public static final Sex MALE;
public static final Sex FEMALE;
public static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
private Sex(String name, int ordinal){
super(name, ordinal);
}
public static Sex[] values(){
return $VALUES.clone();
}
public static Sex valueOf(String name){
return Enum.valueOf(Sex.class, name);
}
}

9 try-with-resources

1
2
3
try(资源变量 = 创建资源对象){
} catch (){
}

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

1
2
3
4
5
6
7
8
9
public class Candy{
public static void main(String[] args){
try(InputStream is = new FileInputStream("d:\\q.txt")){
System.out.println(is);
} catch (IOException e){
e.printStackTrace();
}
}
}

会被转换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Candy{
public Candy(){
}
public static void main(String[] args){
try{
InputStream is = new FileInputStream("d:\\q.txt");
Throwable t = null;
try{
System.out.println(is);
} catch (Throwable e1){
// t 是代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null){
// 如果我们代码有异常
if (t != null){
try{
is.close();
} catch (Throwable e2){
// 如果close有异常,作为被压制异常添加,防止try-with-resourcesfinally里的异常信息的丢失
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close出现的异常就是最后catch块中的e
is.close();
}
}
}
} catch (IOException e){
e.printStackTrace();
}
}
}

10 方法时候的桥接方法

方法重写时候对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类
1
2
3
4
5
6
7
8
9
10
11
12
class A{
public Number m(){
return 1;
}
}
class B extends A{
@Override
// 子类m方法的返回值是Integer是父类m方法返回值Number的子类
public Integer m(){
return 2;
}
}

对于子类,编译器会做如下处理:

1
2
3
4
5
6
7
8
9
10
class B extends A{
public Integer m(){
return 2;
}
// 此方法才是真正重写了父类public Number m()的方法
public synthetic bridge Number m(){
// 调用public Integer m()
return m();
}
}

其中桥接方法比较特殊,仅对Java虚拟机可见,并且与原来的public Integer m()没有命名冲突,可以用下面的反射代码来验证:

1
2
3
for (Method m : B.class.getDeclaredMethods()){
System.out.println(m);
}

11 匿名内部类

  1. 匿名内部类
1
2
3
4
5
6
7
8
9
10
public class Candy{
public static void main(String[] args){
Runnable runnable = new Runnable(){
@Override
public void run(){
System.out.println("OK");
}
};
}
}

转换后代码:

1
2
3
4
5
6
7
8
// 额外生成类
final class Candy$1 implements Runnable{
Candy$1(){
}
public void run(){
System.out.println("OK");
}
}
1
2
3
4
5
public class Candy{
public static void main(String[] args){
Runnable runnable = new Candy$1();
}
}
  1. 引用局部变量的匿名内部类:
1
2
3
4
5
6
7
8
9
10
public class Candy{
public static void test(final int x){
Runnable runnable = new Runnable(){
@Override
public void run(){
System.out.println("OK" + x);
}
};
}
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
// 额外生成类
final class Candy$1 implements Runnable{
int val$x;
Candy$1(int x){
this.val$x = x;
}
public void run(){
System.out.println("OK" + this.val$x);
}
}

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的:因为在创建Candy$1对象时,将x的值赋值给了Candy$q对象的val$x属性,所以x不应该再发生变化了,如果变化,那么val$x属性没有机会再跟着一起变化。