引言
语法糖?想必很多人很多人和我一样在第一次听到这个词的时候都是一头雾水,并不知道这个词是什么意思。我第一次听到这个词的时候也是一脸懵逼,可在我们的日常开发中其实用到语法糖的地方亦是无处不在。最近老是会无意间看到或提起这个词,所以我决定结合反编译的思想来带你们看看到底什么是语法糖。
语法糖介绍
官方回答
语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin(和图灵一样的天才人物,是他最先发现了Lambda演算,由此而创立了函数式编程)发明的一个术语,意指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用阅读。简而言之,语法糖让程序更加简洁,有更高的可读性。
解 “语法糖”
语法糖对于我们来说那是甜蜜蜜肯定写起来舒爽用起来好看[doge],但是对于JVM来说肯定需要细嚼慢咽,一层一层去分析解读他,才能让JVM这个不解风情的家伙读懂!那么会在何时进行解语法糖的操作呢?其实是在编译器将.java文件转化为.class文件时候,会进行解语法糖的操作,将其还原为最为基础的语法操作,以供操作系统处理。
说到编译,大家肯定是知道我们平时写的.java文件如果需要被操作系统所理解需要经过三步骤。
在这里我们所需要理解的是从.java文件到.class文件这一步骤,我们所写的代码通过JDK中的javac编译,将代码变成了jvm所能理解的字节码。在jdk源码包里有这么一个类com.sun.tools.javac.main.JavaCompiler(这个在jdk中的tool.jar中如果在编译器中所引入的jdk的jar包中未找到所需jar包可以去自己安装的jdk中的寻找该jar包自行引入并查看其源码)。你如果有兴趣阅读的话,你会发在该类中的compile2()方法中有个**desugar()方法,顾名思义desugar()**方法就是用来做解语法糖的。以下是部分代码截图
语法糖举例
这些语法糖包含条件编译、Switch语句与枚举及字符串结合、可变参数、自动装箱/拆箱、枚举、内部类、泛型擦除、增强for循环、lambda表达式、try-with-resources语句等等。
一、条件编译
在大多数情况下,程序中的每行代码都是要参与编译的,但是有时出于对代码的优化会对,我们就会对其加上if的条件语句,可能在加上时我们感觉代码量并没有减少多少,但其实在编译的时候只会对满足条件的代码进行编译,将不满足条件的代码自动舍弃,这就是我们要说的条件编译的语法糖。
public static void main(String[] args) {
boolean face = true;
if(face){
System.out.println("你还是要脸的");
}
face = false;
if(false){
System.out.println("你臭不要脸");
}
}
//反编译之后的内容
public static void main(String[] var0) {
boolean var1 = true;
if (var1) {
System.out.println("你还是要脸的");
}
var1 = false;
}
从以上的内容我们可以清晰的看出在反编译之后并没有出现
System.out.println(“你臭不要脸”);这是因为当face为false的时候,编译器就不会出现对其内部代码进行编译。所以,Java语法的条件编译,是通过判断条件为常量的if语句实现的。其原理也就是Java语言的语法糖。
二、switch 支持 String
我们知道在原先的时候java中的switch是只支持基本类型的例如int,char等。对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。由此我们可以看出,其实对于编译器来说,switch中的比较是只能使用整型,任何类型的比较都要转换成整型。比如byte、short、char(ackii码是整型)以及int。在java7之前是不支持String类型的,在java7 之后jdk进行优化,便可以支持String类型的,但其实本质上还是进行整型的比较。读到这里有人可能会疑惑了,String类型是怎样进行整型的比较?那让我们看看其反编译出的代码你就能明白了。
/**
* @author Lucky_J
*/
public class SwitchString {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
switch (str){
case "handsome":
System.out.println("I'm a handsome boy");
break;
case "humorous" :
System.out.println("I'm a humorous boy");
break;
case "intelligent" :
System.out.println("I'm a intelligent boy");
break;
default:
System.out.println("好了你个不要脸的人");
break;
}
}
}
//反编译之后的内容
public class SwitchString {
public SwitchString() {
}
public static void main(String[] var0) {
Scanner var1 = new Scanner(System.in);
String var2 = var1.nextLine();
byte var4 = -1;
switch(var2.hashCode()) {
case 2287075:
if (var2.equals("handsome")) {
var4 = 0;
}
break;
case 553991562:
if (var2.equals("humorous")) {
var4 = 1;
}
break;
case 1134120567:
if (var2.equals("intelligent")) {
var4 = 2;
}
}
switch(var4) {
case 0:
System.out.println("I'm a handsome boy");
break;
case 1:
System.out.println("I'm a humorous boy");
break;
case 2:
System.out.println("I'm a intelligent boy");
break;
default:
System.out.println("好了你个不要脸的人");
}
}
阅读以上代码你会发现原来switch中使用string类型是通过equals()和hashcode()方法实现的,仔细看你会发现switch实际上比较的是hashcode然后通过equals进行安全检查赋值,这一步的equals比较还是十分有必要的,因为可能发生hash碰撞。从上面的反编译代码你就可以明白了其实String类型的switch比较本质上也是进行整型的比较。
三、可变参数
可变参数(variable arguments)是在Java 1.5时引入的一个新特性,它允许一个方法把任意数量的值作为参数。这个新特性的引入极大的简化我们的编程工作。
public class Args {
public static void main(String[] args) {
print("A","B","C");
}
public static void print(String... strings){
for(String s : strings){
System.out.println(s);
}
}
}
//反编译之后的e代码
public class Args {
public Args() {
}
public static void main(String[] var0) {
print("A", "B", "C");
}
public static void print(String... var0) {
String[] var1 = var0;
int var2 = var0.length;0
for(int var3 = 0; var3 < var2; ++var3) {
String var4 = var1[var3];
System.out.println(var4);
}
}
}
从反编译的结果可以看出,可变长参数,是先创建一个数组,数组的长度就是传递的参数的个数,将所传递的参数存入数组。操作该可变长参数实际就变成了对数组的操作。
四、自动拆箱装箱
自动装箱就是Java自动将基本类型转化为相应的包装类对象,例如将long转化为Long这个过程就是装箱,反之就是将Long转为long这个过程称为拆箱
public static void main(String[] args) {
long temp = 10;
Long tempL = temp;
int i = 5;
Integer in = i;
}
//反编译之后内容
public static void main(String[] var0) {
long var1 = 10L;
Long var3 = var1;
byte var4 = 5;
Integer var5 = Integer.valueOf(var4);
}
从反编译出的内容可以看出Integer在自动装箱的时候调用的是Integer.valueOf而拆卸的时候调用的是Integer.intValue。
五、枚举
枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。
public enum Fruit{
APPLE(1),ORANGE(2),BANANA(3);
int code;
Fruit(int code){
this.code=code;
}
}
反编译之后出来的内容
public final class Fruit extends Enum
{
public static Fruit[] values()
{
return (Fruit[])$VALUES.clone();
}
public static Fruit valueOf(String s)
{
return (Fruit)Enum.valueOf(Fruit, s);
}
private Fruit(String s, int i, int j)
{
super(s, i);
code = j;
}
public static final Fruit APPLE;
public static final Fruit ORANGE;
public static final Fruit BANANA;
int code;
private static final Fruit $VALUES[];
static
{
APPLE = new Fruit("APPLE", 0, 1);
ORANGE = new Fruit("ORANGE", 1, 2);
BANANA = new Fruit("BANANA", 2, 3);
$VALUES = (new Fruit[] {
APPLE, ORANGE, BANANA
});
}
}
反编译之后出来的内容
- 定义一个继承自Enum类的Fruit类,Fruit类是用final修饰的
- 为每个枚举实例对应创建一个类对象,这些类对象是用public static final修饰的。同时生成一个数组,用于保存全部的类对象
- 生成一个静态代码块,用于初始化类对象和类对象数组
- 生成一个构造函数,构造函数包含自定义参数和两个默认参数(下文会讲解这两个默认参数)
- 生成一个静态的values()方法,用于返回所有的类对象
- 生成一个静态的valueOf()方法,根据name参数返回对应的类实例(下文会讲解name参数)
六、内部类
内部类之所以可以称之语法糖,是因为他仅仅只是一个编译时的概念,一旦编译成功将会出现两个class文件,例如在test.java内部建立一个内部类反编译会产生test$1inner.class和test.class
public class test {
public static void main(String[] args) {
class inner{
private String name;
private int age;
}
inner i = new inner();
}
}
七、泛型擦除
通常情况下编译器处理泛型拥有两种方式:Code specialization和Code sharing,C++中的模板是典型的Code specialization方式,而Java使用的是Code Sharding的机制。
Code Sharding方式对每个泛型类都产生唯一的一份目标代码,该泛型类的所有实例都映射到这份目标代码上,在需要的时候就会进行类型检查和类型转换。将多种泛型类实例映射到唯一字节码上都是通过**类型擦除(type erasue)**实现的。
也就是说其实对于JVM来说他根本不懂的什么是List list这样的语法需要在编译阶段进行类型擦除的解语法糖操作。‘
类型擦除的步骤:
- 将所有的泛型参数用其最左边界(最顶级的父类型)类替换。
- 移除所有的参数类型
public class Collection {
public static void main(String[] args) {
List<String> list = new ArrayList<String>(10);
list.add("hello world");
Map<String,String> map = new HashMap<String, String>(16);
map.put("name","Lucky_J");
map.put("age","23");
System.out.println(map.get("name"));
System.out.println(map.get("age"));
}
}
//反编译之后的内容
public class Collection {
public Collection() {
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList(10);
var1.add("hello world");
HashMap var2 = new HashMap(16);
var2.put("name", "Lucky_J");
var2.put("age", "23");
System.out.println((String)var2.get("name"));
System.out.println((String)var2.get("age"));
}
}
通过以上反编译出来的代码我们就知道,其实虚拟机当中是没有泛型的,只有普通的类,所有泛型类都会在编译时被擦除。
知识拓展
首先我们来看下类型擦除所会遇到的问题
从以上的代码截图我们可以看出,该类中拥有两个重载函数,因为他们所拥有的特征签名是不同,但是从图中我们可以看出,这段代码是明显编译不通过的,那么现在我们就可以清楚的知道这是为什么了,因为我们从以上讲解中可以知道List<String>和List<Integer>在编译之后进行了类型擦除都变成了原生类型List,导致两个方法的特征签名变得一模一样了。所以在.java文件中是行不通的但是在class文件全是能够这样做的。我们知道,在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。
PS:特征签名就是一个方法中各参数在常量池中字段符号的引用集合,也就是说因为返回值不会包含在特征签名中,所以Java语言中不能仅仅依靠返回值的不同来对一个已有方法进行重载。
但在class文件格式中,特征签名的范围更大一些,只要描述符不完全一致两个方法也是可以共存的。也就是说在class文件格式如果两个方法的特征签名相同但是拥有不同的返回值,也是可以合法的共存于一个Class文件中
八、增强for循环
增强for循环实际上就是for-each循环,想必大家对这个一点都不陌生,因为这是与我们的日常开发所必不可少的东西,他相比较与for循环让我们少写了许多的代码,那么这个语法糖的背后是怎么实现的呢。
public static void main(String[] args) {
String []str = {"A","B","C"};
for(String s : str){
System.out.println(s);
}
}
//反编译之后的内容
public static void main(String[] var0) {
String[] var1 = new String[]{"A", "B", "C"};
String[] var2 = var1;
int var3 = var1.length;
for(int var4 = 0; var4 < var3; ++var4) {
String var5 = var2[var4];
System.out.println(var5);
}
}
代码非常的简单,其实for-each的实现原理就是使用普通的for循环
九、lambda表达式
lambda表达式Java8引入的新特性,从百科上理解lambda表达式其实就是一个匿名函数(但对Java而言其实并不完全是这样).简单地说,它是没有声明的方法,也即没有访问修饰符、返回值声明和名字。
其实你可以把他当作我们上课或者时的关键记忆法。当某个方法只使用一次,而且定义很简短,使用这种速记替代之尤其有效,这样,你就不必在类中费力写声明与方法了。
Java 中的 Lambda 表达式通常使用 (argument) -> (body)
public static void main(String[] args) {
String[] strings = {"KKK", "jinyi", "helloworld", "ceshi"};
List<String> list = Arrays.asList(strings);
list.forEach((str) -> System.out.print(str + "; "));
list.forEach(System.out::println);
}
//反编译之后的内容
public static void main(String[] var0) {
String[] var1 = new String[]{"KKK", "jinyi", "helloworld", "ceshi"};
List var2 = Arrays.asList(var1);
var2.forEach((var0x) -> {
System.out.print(var0x + "; ");
});
PrintStream var10001 = System.out;
System.out.getClass();
var2.forEach(var10001::println);
}
其实从以上反编译出来的内容并不能给我们太多的信息让我们知道编译器到底为我们做了什么,我们可以在cmd窗口中敲出javap -p Lambda来得到以下信息
从图中我们发现与源代码相比较多出了一个lambda$main$0的私有方法
再者执行代码的时候你如果你使用的是IDEA可以在VM options栏加上-Djdk.internal.lambda.dumpProxyClasses参数会发现在项目的最外层生成了两个class文件Lambda$Lambda 1. c l a s s 和 L a m b d a 1.class和Lambda 1.class和Lambda$Lambda$2.class它是通过LambdaMetaFactory的metafactory方法动态生成的。反编译代码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package sugar;
import java.lang.invoke.LambdaForm.Hidden;
import java.util.function.Consumer;
// $FF: synthetic class
final class Lambda$$Lambda$1 implements Consumer {
private Lambda$$Lambda$1() {
}
@Hidden
public void accept(Object var1) {
Lambda.lambda$main$0((String)var1);
}
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package sugar;
import java.io.PrintStream;
import java.lang.invoke.LambdaForm.Hidden;
import java.util.function.Consumer;
// $FF: synthetic class
final class Lambda$$Lambda$2 implements Consumer {
private final PrintStream arg$1;
private Lambda$$Lambda$2(PrintStream var1) {
this.arg$1 = var1;
}
private static Consumer get$Lambda(PrintStream var0) {
return new Lambda$$Lambda$2(var0);
}
@Hidden
public void accept(Object var1) {
this.arg$1.println((String)var1);
}
}
通过以上的代码就可以知道Lambda表达的语法糖是通过内部类+静态方法实现的了(为什么是内部类,因为掉用了Lambda类中的私有方法lambda$main$0,还有为什么lambda表达式引用的局部变量不能改变的原因也显而易见,因为局部变量是final类型的)。
十、try-with-resources语句
Java里对于文件流操作,数据库连接等操作都是异常昂贵的,用完之后必须及时关闭,否者一直处于打开状态,可能会导致内存泄漏等问题。
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream(new File("D:\\test.txt"))) {
System.out.println(inputStream.read());
} catch (IOException e) {
e.printStackTrace();
}
}
//反编译之后的代码
public static void main(String[] var0) {
try {
FileInputStream var1 = new FileInputStream(new File("D:\\test.txt"));
Throwable var2 = null;
try {
System.out.println(var1.read());
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (var1 != null) {
if (var2 != null) {
try {
var1.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
var1.close();
}
}
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
从以上反编译的内容我们可以看出,其实try-with-resource的原理也很简单,就是我们未关闭的资源编译器帮我们关闭了。这也再次证明了语法糖的作用就是方便程序员使用,但是最终还是要转化为编译器所认识的代码。
总结
本文总共介绍了十种Java常用的语法糖,从我们上面的讲解我们也可以明白了,其实所谓的语法糖就是方便程序员使用,但语法糖只有开发人员能够享受其带来的好处,如果要想被Jvm所认识还需经过一步解语法糖的操作,在我们一步步拆解语法糖的时候,你会发现其实我们用的这些语法糖,就是由一些更为简单的代码所组成的。
好了本篇文章的介绍就到此结束,感谢大家的阅读!!!