语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
Java中最常用的语法糖主要是泛型、变长数组、自动装箱/拆箱等等,Java虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖(语法糖被解除)。
编译:
前端编译器把`*.java`文件变成`*.class`文件的过程;
后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程;
静态前端编译器(AOT编译器,Ahead Of Time Compiler)直接把 *.java
文件编译成本地机器代码的过程;
本章只需要前端编译。
反编译:把*.class
文件变成*.java
文件的过程;
本章使用cfr_0_132.jar
,下载地址cfr_0_132
泛型是JDK1.5的一项新特性,它的本质是参数化类型的应用,也就是说所有操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中。分别称为泛型类、泛型接口、泛型方法。
如:ArrayList
、Map
、public
。
Java 语言中的泛型,它只在程序的源码中存在,在编译后的字节码文件中,就是已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转换代码,因此,对于运行期的java语言来说,ArrayList
与ArrayList
就是同一个类,所以泛型技术实际上是java语言的一颗语法糖,java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
代码清单1是一段简单的java泛型的例子,我们可以看下代码清单2编译后的结果。
代码清单1:泛型
private static void demo1() {
Map map = new HashMap<>();
map.put("hello", "你好!");
map.put("how are you", "吃了没?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you"));
}
代码清单2:反编译结果
java -jar cfr_0_132.jar class文件目录
private static void demo1() {
HashMap map = new HashMap();
map.put("hello", "你好!");
map.put("how are you", "吃了没?");
System.out.println((String)map.get("hello"));
System.out.println((String)map.get("how are you"));
}
可以看到map.get
返回类型被强转成String
了。
这就可以解释非泛型与泛型间转换时为什么会出现警告,有可能在强制转换时出现ClassCastException
异常,所以在这中转换时一定要小心。如:代码清单3 List 泛型与非泛型间转换。
代码清单3:
private static void demo2() {
List list = createList();
list.add("1");
list.add("2");
list.add("3");
for (int i = 0; i < list.size(); i++) {
String str = list.get(i);
System.out.println(str);
}
}
private static List createList() {
List list = new ArrayList();
list.add(1);
list.add(new Date());
return list;
}
出现ClassCastException
异常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.example.syntactic.sugar.Genericity.demo2(Genericity.java:24)
at com.example.syntactic.sugar.Genericity.main(Genericity.java:15)
包装类型转换成基本数据类型叫做拆箱,基本数据类型转换成包装类型叫做装箱,自java1.5
以后编译器帮助我们完成装箱/拆箱叫自动装箱/拆箱。如:Integer->int基本类型是拆箱,反之int->Integer类型是装箱。
代码清单4:自动装箱/拆箱
private static void demo1() {
Integer[] ary = new Integer[8];
for (int i = 0; i < ary.length; i++) {
ary[i] = i;
}
for (int i = 0; i < ary.length; i++) {
int value =ary[i];
System.out.println(value);
}
}
反编译:自动装箱/拆箱反编译结果
java -jar cfr_0_132.jar class文件目录 –sugarboxing false
private static void demo1() {
int i;
Integer[] ary = new Integer[8];
for (i = 0; i < ary.length; ++i) {
ary[i] = Integer.valueOf((int)i);//自动装箱
}
for (i = 0; i < ary.length; ++i) {
int value = ary[i].intValue();//自动拆箱
System.out.println((int)value);
}
}
反编译后我们发现自动装箱就是使用Integer.valueOf
,自动拆箱Integer.intValue
,如果是Long类型就是Long.valueOf
和 Long.intValue
等等。
下列代码的运行结果什么?
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a + b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));
ForEach
循环相信大家都很熟悉了,使用ForEach
循环必须是数组或实现Iterable
接口,最常用的就是List
集合使用ForEach
循环遍历元素了,那么它是如何实现的呢,我们通过反编译看看就什么都知道了。
代码清单5:List集合使用ForEach
循环
//源码
private static void demo1() {
List list = new LinkedList<>();
list.add("a");
list.add("b");
list.add("c");
for (String str : list) {
System.out.println(str);
}
}
//
反编译:是不是一目了然,while
循环 + Iterator
迭代接口,处理ForEach
java -jar cfr_0_132.jar class文件目录 –collectioniter false
private static void demo1() {
LinkedList list = new LinkedList();
list.add("a");
list.add("b");
list.add("c");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String str = (String)iterator.next();
System.out.println(str);
}
}
结论:只要类继承了Iterable
接口就可以使用ForEach
循环。
代码清单6:Integer
数组使用ForEach
循环
private static void demo2() {
Integer[] array = {1,2,3,4};
for (Integer integer : array) {
System.out.println(integer);
}
}
反编译:数组和List
集合遍历的方式就有点不同了,使用的是标准的fori
循环。
java -jar cfr_0_132.jar class文件目录 –arrayiter false
private static void demo2() {
Integer[] array;
Integer[] arrinteger = array = new Integer[]{1, 2, 3, 4};
int n = arrinteger.length;
for (int i = 0; i < n; ++i) {
Integer integer = arrinteger[i];
System.out.println(integer);
}
}
同样枚举也是java1.5
版本添加的类型,使用关键字enum
可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。
通过反编译揭开枚举的面纱。
代码清单7:非常简单的枚举类型
public enum Enumeration {
SUNDAY,
MONDAY,
}
反编译:
java -jar cfr_0_132.jar class文件目录 –sugarenums false
public final class Enumeration
extends Enum<Enumeration> {
public static final /* enum */ Enumeration SUNDAY = new Enumeration();
public static final /* enum */ Enumeration MONDAY = new Enumeration();
private static final /* synthetic */ Enumeration[] $VALUES;
public static Enumeration[] values() {
return (Enumeration[])$VALUES.clone();
}
public static Enumeration valueOf(String name) {
return Enum.valueOf(Enumeration.class, name);
}
private Enumeration() {
super(string, n);
}
static {
$VALUES = new Enumeration[]{SUNDAY, MONDAY};
}
}
反编译后可以解释很多问题
1、final class
不可被继承。
2、extends Enum
继承了Enum
类,枚举实例可以使用Enum
的方法,Enum
实现Serializable
接口,所以枚举是可以进行序列化的,参考深度分析Java的枚举类型—-枚举的线程安全性及序列化问题
3、public static final
修饰了实例,所以每个枚举实例都是一个全局常量。
4、values()
、valueOf(String name)
静态方法
5、private Enumeration()
构造器私有
上面是最简单的枚举的应用,接下来编写一个复杂点的枚举。
代码清单8:复杂枚举类型,实现接口、成员变量、枚举实例实现抽象方法。
public enum Enumeration implements KeyValue {
SUNDAY("周日", 0) {
@Override
public String details() {
return "一周的最后一天";
}
},
MONDAY("周一", 1) {
@Override
public String details() {
return "一周的第一天";
}
};
private final String name;
private final Integer code;
Enumeration(String name, Integer code) {
this.name = name;
this.code = code;
}
public abstract String details();
@Override
public PropertyEntity getPropertyEntity() {
return new PropertyEntity(code, name);
}
}
反编译:
java -jar cfr_0_132.jar class文件目录 –sugarenums false
public abstract class Enumeration
extends Enum<Enumeration>
implements KeyValue {
public static final /* enum */ Enumeration SUNDAY = new Enumeration("SUNDAY", 0, "\u5468\u65e5", Integer.valueOf(0)){
@Override
public String details() {
return "\u4e00\u5468\u7684\u6700\u540e\u4e00\u5929";
}
};
public static final /* enum */ Enumeration MONDAY = new Enumeration("MONDAY", 1, "\u5468\u4e00", Integer.valueOf(1)){
@Override
public String details() {
return "\u4e00\u5468\u7684\u7b2c\u4e00\u5929";
}
};
private final String name;
private final Integer code;
private static final /* synthetic */ Enumeration[] $VALUES;
public static Enumeration[] values() {
return (Enumeration[])$VALUES.clone();
}
public static Enumeration valueOf(String name) {
return Enum.valueOf(Enumeration.class, name);
}
private Enumeration(String name, Integer code) {
super(string, n);
this.name = name;
this.code = code;
}
public abstract String details();
@Override
public KeyValue.PropertyEntity getPropertyEntity() {
return new KeyValue.PropertyEntity(this.code, this.name);
}
static {
$VALUES = new Enumeration[]{SUNDAY, MONDAY};
}
}
1、类不是final
而是abstract
抽象类,其实原理很简单,因为枚举类details
方法是抽象方法,所以枚举类中包含抽象方法,编译后就是抽象类。
2、每个实例必须实现details
抽象方法,以匿名内部类的方式。
3、当然枚举也可以实现一个或多个接口。
switch
java6
开始支持支持枚举类型,java8
开始支持String类型,其实这个些都是表象,我们通过反编译解释下
代码清单9:String 与枚举使用switch
private static void demo2() {
String str = "a";
switch (str) {
case "a":
System.out.println("这是 a ");
break;
case "b":
System.out.println("这是 b ");
break;
default:
System.out.println(" 没找到 String case");
}
}
private static void demo3() {
Enumeration monday = Enumeration.MONDAY;
switch (monday) {
case MONDAY:
System.out.println(monday.details());
break;
case SUNDAY:
System.out.println(monday.details());
break;
}
}
反编译:
java -jar cfr_0_132.jar class文件目录 --decodeenumswitch false --decodestringswitch false
private static void demo2() {
String str;
String string = str = "a";
int n = -1;
switch (string.hashCode()) {
case 97: {
if (!string.equals("a")) break;
n = 0;
break;
}
case 98: {
if (!string.equals("b")) break;
n = 1;
}
}
switch (n) {
case 0: {
System.out.println("\u8fd9\u662f a ");
break;
}
case 1: {
System.out.println("\u8fd9\u662f b ");
break;
}
default: {
System.out.println(" \u6ca1\u627e\u5230 String case");
}
}
}
private static void demo3() {
Enumeration monday = Enumeration.MONDAY;
switch (.$SwitchMap$com$example$syntactic$sugar$Enumeration[monday.ordinal()]) {
case 1: {
System.out.println(monday.details());
break;
}
case 2: {
System.out.println(monday.details());
}
}
}
反编译后可以看出,String
使用了两个switch
实现的。
第一个switch
使用String
的hashCode
和equals
方法给int
类型变量n
(存在变量n
,就不使用n
作为变量)赋值。
第二个switch
根据变量n
判读执行那个case
。
反编译后可以看出:
switch (monday)
被替换成Switch$1.$SwitchMap$com$example$syntactic$sugar$Enumeration[monday.ordinal()]
Switch$1.$SwitchMap$com$example$syntactic$sugar$Enumeration
:编译器生成的一个数组,下标是monday.ordinal()
值还是monday.ordinal()
,所以枚举switch
实际上就是利用的monday.ordinal()
方法返回的int
值的。
可变参数是java5
加入新的语法,支持同一类型可以是任意数量
代码清单10:方法变长参数的使用
private static void demo1() {
method1();
method1("123");
method1("123", "456");
}
private static void method1(String... str) {
System.out.println(Arrays.toString(str));
}
反编译:
java -jar cfr_0_132.jar class文件目录
private static void demo1() {
VariableMethod.method1(new String[0]);
VariableMethod.method1(new String[]{"123"});
VariableMethod.method1(new String[]{"123", "456"});
}
private static /* varargs */ void method1(String[] str) {
System.out.println(Arrays.toString(str));
}
反编译后发现可变长方法参数其实就是一个数组类型。
条件编译很简单,使用if
语句实现,代码清单10
代码清单11:条件编译
private static boolean conditional;
private final static boolean conditional_final = true;
private final static int conditional_int = 1;
public static void main(String[] args) {
if (true) {
System.out.println("if inner");
}
if (false) {
System.out.println("if inner 1");
} else {
System.out.println("else inner 1");
}
boolean b = false;
if (b) {
System.out.println("if inner 2");
} else {
System.out.println("else inner 2");
}
if (conditional) {
System.out.println("conditional if");
}
if (conditional_final) {
System.out.println("conditional_final if" );
}
if (conditional_int == 1) {
System.out.println("conditional_int == 1");
}
}
反编译:
java -jar cfr_0_132.jar class文件目录
private static boolean conditional;
private static final boolean conditional_final = true;
private static final int conditional_int = 1;
public static void main(String[] args) {
System.out.println("if inner");
System.out.println("else inner 1");
boolean b = false;
if (b) {
System.out.println("if inner 2");
} else {
System.out.println("else inner 2");
}
if (conditional) {
System.out.println("conditional if");
}
System.out.println("conditional_final if");
System.out.println("conditional_int == 1");
}
反编译后可以发现,if
或else
语句内在编译期能确定一定执行,编译器就去掉没有必要的代码块。
在Java 7
中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。
代码清单12:数值字面量 使用_
分割
private static void demo1() {
//一亿
int i = 1_0000_0000;
System.out.println(i);
}
反编译:
java -jar cfr_0_132.jar class文件目录
private static void demo1() {
int i = 100000000;
System.out.println(i);
}
lambda
是java8
新加入的一种语法,使我们的代码更简洁,开发效率更高,但对于刚接触lambda
的开发人员可能不太会用,idea
开发工具会为我们提示那些代码可以转换成lambda
表达式。
既然是语法糖,JVM
肯定是没有对改语法的支持,那么它是如何实现的呢,我们通过反编译看看就知道了。
代码清单13:lambda表达式的简单使用
private static void demo1() {
List list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.stream()
.peek(s -> {
System.out.println("内容:");
System.out.println(s);
})
.map(Integer::valueOf)
.forEach(System.out::println);
}
反编译:
java -jar cfr_0_132.jar class文件目录 --decodelambdas false
private static void demo1() {
ArrayList list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
PrintStream printStream = System.out;
printStream.getClass();
list.stream()
.peek(
(Consumer)LambdaMetafactory.metafactory
(null, null, null, (Ljava/lang/Object;)V,
//方法体
lambda$demo1$0(java.lang.String), (Ljava/lang/String;)V)()
).map(
(Function)LambdaMetafactory.metafactory
(null, null, null, (Ljava/lang/Object;)Ljava/lang/Object;,
//Integer::valueOf
valueOf(java.lang.String),(Ljava/lang/String;)Ljava/lang/Integer;)()
).forEach(
(Consumer)LambdaMetafactory.metafactory
(null, null, null, (Ljava/lang/Object;)V,
//System.out::println
println(java.lang.Object), (Ljava/lang/Integer;)V)((PrintStream)printStream)
);
}
private static /* synthetic */ void lambda$demo1$0(String s) {
System.out.println("\u5185\u5bb9\uff1a");
System.out.println(s);
}
其实我们只需关心以下两点:
1、lambda
表达式使用的不是内部类,是lambda
API LambdaMetafactory
实现的。
2、lambda
代码块(()->{})在编译期间生成了对应的静态方法如:lambda$demo1$0
,双冒号就是调用调用对应的方法,如:System.out::println
更多请参考 :Translation of Lambda Expressions
java7
开始提供了一种新的关闭资源的方式,try-with-resource
被关闭资源只需实现AutoCloseable
如:代码清单13
代码清单14:try-with-resource关闭资源
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("file.xml"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
}
}
反编译
java -jar cfr_0_132.jar class文件目录 --tryresources false
public static void main(String[] args) {
try {
BufferedReader br = new BufferedReader(new FileReader("file.xml"));
Throwable throwable = null;
try {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
catch (Throwable line) {
throwable = line;
throw line;
}
finally {
if (br != null) {
if (throwable != null) {
try {
br.close();
}
catch (Throwable line) {
throwable.addSuppressed(line);
}
} else {
br.close();
}
}
}
}
catch (IOException br) {
// empty catch block
}
}
通过观察反编译代码可以看到,其实try-with-resource
就是通过try+catch+finally
实现资源关闭的。
其实java
不断升级的过程中添加了很多语法糖,其背后隐藏了很多我们看不到东西,相信同学们看完篇文章应该对其语法糖也有了一些了解,在看到新的语法用反编译看看它是如何实现的,这样在使用的时候更加得心应手了。