Java语法糖

语法糖

语法糖(Syntactic Sugar),是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法。这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。

语法盐(Syntactic Salt)是指在计算机语言设计中,不容易产生不良代码的特性。可以为容易犯的语法错误加上的额外语法限制,比如类型检查。

语法糖精也叫语法糖浆,指的是未能让编程更加方便的附加语法,一说是设计失败的语法糖。这个语法又麻烦又没用。

语法海洛因,是指过于喜欢操作符重载,使得程序表面上紧凑。操作符重载就是把已经定义的、有一定功能的操作符进行重新定义,来完成更为细致具体的运算等功能。操作符重载可以将概括性的抽象操作符具体化,便于外部调用而无需知晓内部具体运算过程。

我们所熟知的编程语言中几乎都有语法糖。很多人说 Java 是一个 “低糖语言”,其实从 Java 7 开始 Java 语言层面上一直在添加各种糖,主要是在 “Project Coin” 项目下研发。尽管现在 Java 有人还是认为现在的 Java 是低糖,未来还会持续向着 “高糖” 的方向发展。

解语法糖

语法糖的存在主要是方便开发人员使用。但其实,Java 虚拟机并不支持这些语法糖,这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。

com.sun.tools.javac.main.JavaCompiler 的源码中,compile() 有一个步骤就是调用 desugar() ,这个方法就是负责解语法糖的。

Java 中最常用的语法糖主要有 switch 支持 String 与枚举、泛型、自动装箱与拆箱、方法变长参数、枚举、内部类、条件编译、 断言、数值字面量、foreach、try-with-resource、Lambda表达式等

switch 支持 String 与枚举

进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行 switch 或者使用纯整数常量,但这也不是很差。

参考 String分析

泛型

不同的编译器对于泛型的处理方式是不同的。通常情况下,一个编译器处理泛型有两种方式:Code specializationCode sharing。C++ 和 C#是使用 Code specialization 的处理机制,而 Java 使用的是 Code sharing 的处理机制。

Code sharing 方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

也就是说,对于 Java 虚拟机来说,他根本不认识 Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。

参考 Java泛型解析

自动装箱与拆箱

自动装箱就是 Java 自动将原始类型值转换成对应的对象,比如将 int 的变量转换成 Integer 对象,这个过程叫做装箱。

反之将 Integer 对象转换成 int 类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。

原始类型 byte、short、char、int、long、float、double 和 boolean 对应的封装类为 Byte、Short、Character、Integer、Long、Float、Double、Boolean。

1
2
3
4
public static void main(String[] args) {        
int i = 10;
Integer n = i; // 装箱:Integer n = Integer.valueOf(i);
}
1
2
3
4
public static void main(String[] args) {        
Integer i = 10; // Integer i = Integer.valueOf(10);
int n = i; // 拆箱:int n = i.intValue();
}

方法变长参数

可变参数 (variable arguments) 是在 Java 1.5 中引入的一个特性,它允许一个方法把任意数量的值作为参数。

可变参数在被使用的时候:

  1. 首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数。
  2. 然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。

枚举

Java SE5 提供了一种新的类型-Java 的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。

当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。

参考 枚举类型

内部类

内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念。

Outer.java 里面定义了一个内部类 Inner,一旦编译成功,就会生成两个完全不同的 .class 文件了,分别是 Outer.class 和 Outer$Inner.class。所以内部类的名字完全可以和它的外部类名字相同

匿名内部类也会被当作普通的类处理,只不过编译器生成它构造方法的时候,除了将外部类的引用传递了过来,还将基本数据类型的变量复制了一份过来,并把引用数据类型的变量引用也传递了过来。因此,基本数据类型的变量不能修改,不然就会跟外部的变量产生不一致,这样的话变量的传递也就变得毫无意义。

final 关键字除了能让类不能被继承之外,对应到这种场景,就是让变量也不能被重新赋值。

条件编译

C、C++ 等许多语言提供了预处理的功能,并通过预处理来实现条件编译。Java 并没有提供类似的预处理功能,但是 Java 也可以实现条件编译。

1
2
3
4
5
6
7
8
private static final boolean DEBUG = true;
public static void main(String[] args) {
if (DEBUG) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}

编译后即

1
2
3
public static void main(String[] args) {
System.out.println("block 1");
}

断言语句

断言在编译过后被转化成了语句所处类中的一个 static final boolean 字段,并在类初始化阶段借助静态语句块完成其初始化。在程序执行过程中,直接根据该字段判断断言是否开启,来决定是否执行断言检查。

数值字面量

支持的数字字面量表示

十进制:默认的。

八进制:整数之前加数字 0 来表示。

十六进制:整数之前加“0x”或“0X”来表示。

二进制:整数之前加“0b”或“0B”来表示。

在数值字面量中使用下划线

在 Java 7 中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。
比如:

1
2
3
1_500_000 
5_6.3_4
89_3___1

下划线只能出现在数字中间,前后必须是数字。所以_1000b_101是不合法的,无法通过编译。
这样限制的动机就是可以降低实现的复杂度。有了这个限制,Java 编译器只需在扫描源代码的时候将所发现的数字中间的下划线直接删除就可以了。如果不添加这个限制,编译器需要进行语法分析才能做出判断。比如:_100,可能是一个整数字面量 100,也可能是一个变量名称。这就要求编译器的实现做出更复杂的改动。

foreach

  • 对有实现 Iterable 接口的对象采用 foreach 语法糖的话,编译器会将这个 for 关键字转化为对目标的迭代器使用。如果要想使自己自定义的类可以采用foreach语法糖就必须实现Iterable接口。
  • 对于数组而言,其实就是转换为普通的遍历。
  • 对于实现 RandomAccess 接口的集合比如 ArrayList,应当使用最普通的 for 循环而不是 foreach 循环来遍历。

实现 RandomAccess 接口的类实例,假如是随机访问的,使用普通 for 循环效率将高于使用 foreach 循环;反过来,如果是顺序访问的,则使用 Iterator 会效率更高。

1
2
3
4
5
6
7
8
9
10
if (list instanceof RandomAccess) { 
for (int i = 0; i < list.size(); i++){

}
} else {
Iterator<?> iterator = list.iterable();
while (iterator.hasNext()){
iterator.next()
}
}

try-with-resource

try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭。编译之后会判断对象是否为 null,如果不是 null,则调用 close 函数进行资源回收。

所有实现了 java.lang.AutoCloseable 接口(其中,它包括实现了 java.io.Closeable 的所有对象),可以使用作为资源。不同的是 java.io.Closable 要求实现者保证 close 函数可以被重复调用。而 java.lang.AutoCloseable 的close 函数则不要求是幂等的。

try-with-resources 声明在 JDK 9 已得到改进。如果你已经有一个资源是 final 或等效于 final 变量,您可以在 try-with-resources 语句中使用该变量,而无需在 try-with-resources 语句中声明一个新变量。

Lambda表达式

TODO 待补充

反编译

javap 是 JDK 自带的反汇编器,可以查看 java 编译器为我们生成的字节码。通过它,我们可以对照源代码和字节码,从而了解很多编译器内部的工作。

语法

  javap [option] class
javap 命令用于解析类文件。其输出取决于所用的选项。若没有使用选项,javap 将输出传递给它的类的 public 域及方法。javap 将其输出到标准输出设备上。

命令选项

-help 输出 javap 的帮助信息。
-l 输出行及局部变量表。
-b 确保与 JDK 1.1 javap 的向后兼容性。
-public 只显示 public 类及成员。
-protected 只显示 protected 和 public 类及成员。
-package 只显示包、protected 和 public 类及成员。这是缺省设置。
-private 显示所有类和成员。
-J[flag] 直接将 flag 传给运行时系统。
-s 输出内部类型签名。
-c 输出类中各方法的未解析的代码,即构成 Java 字节码的指令。
-verbose 输出堆栈大小、各方法的 locals 及 args 数,以及class文件的编译版本
-classpath[路径] 指定 javap 用来查找类的路径。如果设置了该选项,则它将覆盖缺省值或 CLASSPATH 环境变量。目录用冒号分隔。