语法糖,又称糖衣语法,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。之所以称作是“糖”,是因为它可以使代码写起来更方便,看起来更简洁,就像是给代码里面加了糖一样,越写越开心。
与之相对的是语法盐,就是虽然使用这种语法特性能够使写出坏代码的可能性降低,但这些特性会强迫程序员做出一些基本不用描述程序行为,而是用来证明他们知道自己在做什么的额外举动,总之就是咸得让人不快乐。
不过,虽然语法糖的存在能使开发变得更加方便,但实际上 Java 虚拟机并不支持这些语法糖。它们在编译阶段就会被还原成 Java 的基础语法结构,这个过程也被称作解语法糖。
那么就让我们来解一下 Java 中的语法糖,看看这些糖块的真面目吧!(o゚ω゚o)
在 Java7 以前,能被 switch 支持的参数类型仅有 int
、 short
、 char
、 byte
和 枚举
这五种。对于编译器来说,switch 中其实只能使用整型参数,而它对 char 类型的支持,也是通过对其 ASCII 码进行比较来实现的。
不过,从 JDK1.7 开始,switch 中又添加了对 String 的支持,我们来写段代码看一下 :
package demo; public class Demo { public static void main(String[] args) { String s = "hello"; switch (s) { case "hello" : System.out.println("hello"); break; case "world" : System.out.println("world"); break; default: } } } 复制代码
对上述代码进行反编译后,得到了如下结果 :
可以看到,对字符串的 switch 支持其实是通过 hashCode()
和 equals()
实现的。值得注意的是,这里用 equals() 进行了必要的二次校验,这是为了防止哈希碰撞,即哈希码相同而对象不同的情况。
再来写一段 enum 的 :
package demo; public class Demo { public static void main(String[] args) { DemoEnum e = DemoEnum.UP; switch (e) { case UP : System.out.println("hello"); break; case DOWN : System.out.println("world"); break; default: } } } enum DemoEnum { UP, DOWN, LEFT, RIGHT } 复制代码
这个要结合枚举类的反编译看,后面会详细讲到。枚举类的实现原理是相当于给它补了一个 int 类型的 code码,而 switch 在对 enum 进行比较时,实际上就是使用的这个 code 码。
对 Java 虚拟机来说,“泛型” 是一种不存在的东西。像类似 List<String>
这样的语法,在编译期间就已经进行了名为类型擦除的解语法糖。
类型擦除主要分为两个步骤 :
所以,在对一段使用了泛型的代码进行反编译后,我们会得到这样的结果 :
package demo; import java.util.ArrayList; import java.util.List; public class Demo { public static void main(String[] args) { List<Student> list = new ArrayList<>(); list.add(new Student("a", 1)); } } class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } } 复制代码
List<Student>
被完全擦除变为 List
,但这并非没有意义的。使用泛型能够在编译期间就对代码规范进行限制,从而避免了一个声明 Student 的 List 里面被误加入了一个 Teacher。
自动装箱是指将 Java 中的原始类型自动转换为对应的封装类型,自动拆箱则反之。具体的类型对应有以下八种 : byte - Byte
、 short - Short
、 char - Character
、 int - Integer
、 long - Long
、 float - Float
、 double - Double
、 boolean - Boolean
。
package demo; public class Demo { public static void main(String[] args) { // 装箱 Integer a = 11; System.out.println(a); // 拆箱 int b = a; System.out.println(b); } } 复制代码
撸一个简单的装箱拆箱就可以看出来,装箱是通过调用包装器的 valueOf()
方法实现的,而拆箱是通过调用 xxxValue()
实现的。
可变参数是在 Java5 中引入的一个特性,它允许一个方法把任意数量的值作为参数。
package demo; public class Demo { public static void main(String[] args) { } private static void demo(String... args) { for(String s : args) { System.out.println(s); } } } 复制代码
由上,可变参数在被使用时,将首先创建一个长度为实际传递的参数个数的数组,并将参数值放入该数组中;而被调用的方法声明的参数列表,实际上也被编译为了一个数组。
都说 Java 中一切皆对象,对象得有个类呀,那么,枚举的类在哪里呢?先来写一个简单的枚举类 :
package demo; public enum DemoEnum { UP, DOWN, LEFT, RIGHT } 复制代码
然后对它进行反编译 :
可以看到,枚举是由一个编译器自动创建的类 public final class XXEnum extends Enum
所维护的,而我们定义的具体枚举被声明为了类中的静态常量。该类继承了 Enum
,同时用 final
关键字修饰着,这也是为什么说枚举类型不能被继承的根本原因所在。
内部类又称嵌套类,相当于外部类中的一个普通成员。
然而事实上,内部类仅仅是一个编译时期的概念。虽然是作为外部类的一个“成员”存在的,但在实际编译的过程中,它将作为一个独立的类存在,并生成一个命名为 外部类名$内部类名.class
,且不依存于外部类的 .class 文件。
package demo; public class Demo { class InnerClass { private String name; private int age; } } 复制代码
不过,当我们对外部类进行反编译的时候,还是会连着内部类的 .class 文件一起打包进行反编译的 OwO
一般情况下,程序中的每行代码都是需要参与编译的。但有时出于对代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,而将不满足条件的舍弃,这就是条件编译。
比如下面这段代码。在已知 flag 为 true 的情况下,代码逻辑将一定不会进入输出 false 的分支,所以,在编译的时候,确认不会进入的分支代码将被直接舍弃,整个条件从句被简化为了直接执行 true 所在的分支。
package demo; public class Demo { public static void main(String[] args) { final boolean flag = true; if(flag) { System.out.println("true"); } else { System.out.println("false"); } } } 复制代码
在 Java 中, assert
关键字是从 JAVA SE 1.4 开始引入的,为了避免和老板本中的一些冲突,Java 在执行的时候默认是不启动断言检查的,即默认忽略所有断言语句。如果需要开启断言,可以通过设置 -enableassertions
或 -ea
来达到目的。
package demo; public class Demo { public static void main(String[] args) { int a = 1, b = 1; assert a == b; System.out.println("a == b"); assert a != b : "false"; System.out.println("a != b"); } } 复制代码
从反编译之后的代码中可以看出,断言的底层实现就是 if 语句 :如果断言结果为真,则什么都不做,程序继续执行;如果断言的结果不为真,则抛出 AssertError 来打断执行。
数值字面量,指在数字 ( 整型或浮点数都可以 ) 之间插入任意多个下划线,以方便开发者的阅读,但不会影响程序的编译。它的原理就是,编译的时候把下划线删掉 QvQ
package demo; public class Demo { public static void main(String[] args) { int a = 100_00; System.out.println(a); } } 复制代码
增强 for 循环,能让 for 循环变得更加简洁明了的循环,它的实现原理是使用了普通的 for 循环和迭代器 qwq
package demo; import java.util.ArrayList; import java.util.List; public class Demo { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); for(Integer i : list) { System.out.println(i); } } } 复制代码
带资源的 try-catch,能帮我们处理在进行一些操作,尤其是文件操作和数据库连接等时候,对其中使用到的一些资源的关闭。相比于使用普通 try-catch 时在 finally 中释放资源,使用 try-with-resource 能够避免这种繁琐且重复的 close() 工作,从而使代码变得简洁易读。
package demo; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; public class Demo { public static void main(String[] args) { try (BufferedReader br = new BufferedReader(new FileReader("path"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { } } } 复制代码
使用反编译对代码进行还原之后就会发现,其实 try-with-resource 的底层实现原理依然是传统的关闭方式,即是我们没有做的资源关闭工作,编译器帮我们干掉了。
最后是我们的 lambda 表达式了!它的实现原理是调用了 JVM 底层提供的 lambda 相关 API。比如这里,就是调用了 java.lang.invoke.LambdaMetafactory#metafactory
方法,然后使用一个 lambda$main$0
方法进行输出 :
package demo; import java.util.Arrays; import java.util.List; public class Demo { public static void main(String[] args) { String[] str = {"aa", "bb", "cc"}; List<String> list = Arrays.asList(str); list.forEach((s) -> System.out.println(s)); } } 复制代码