语法糖--这一篇全了解 浅浅的花香味﹌ 2022-05-23 01:00 121阅读 0赞 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。 解语法糖 前面提到过,语法糖的存在主要是方便开发人员使用。但其实,**Java虚拟机并不支持这些语法糖**,这些语法糖在**编译阶段就会被还原成简单的基础语法结构**,这个过程就是解语法糖。 说到编译,大家肯定都知道,Java语言中,javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。如果你去看com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用**desugar()**,这个方法就是负责解语法糖的实现的。 Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理,一步一步剥去糖衣,看看其本质。 ## 糖块一、 switch 支持 String 与枚举 ## 前面提到过,从Java 7 开始,Java语言中的语法糖在逐渐丰富,其中一个比较重要的就是Java 7中switch开始支持String。在开始coding之前先科普下,Java中的swith自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。所以,对于编译器来说,**switch中其实只能使用整型**,任何类型的比较都要转换成整型。比如byte、short、char(ackii码是整型)以及int。 那么接下来看下switch对String得支持,有以下代码: public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; default: break; } } } 反编译后内容如下: public class switchDemoString{ public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; } } } 看到这个代码,你知道原来**字符串的switch是通过equals()和hashCode()方法来实现**的。还好hashCode()方法返回的是int,而不是long。 仔细看下可以发现,**进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查**,这个检查是**必要的,因为哈希可能会发生碰撞**。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。 ## 糖块二、 泛型 ## 我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的,通常情况下,一个编译器处理泛型有两种方式:Code specialization和Code sharing。C++和C\#是使用Code specialization的处理机制,而Java使用的是Code sharing的机制。 Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过**类型擦除(type erasue)实现的**。 也就是说,对于**Java虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖**。 类型擦除的主要过程如下: **将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。** **移除所有的类型参数**。 以下代码: Map<String, String> map = new HashMap<String, String>(); map.put("name", "hollis"); 解语法糖之后会变成: Map map = new HashMap(); map.put("name", "hollis"); 虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。 **扩展**:java语言中重载与返回值无关;重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名中,所以返回值不参与重载选择,但是在Class文件 格式中,只要描述符不是完全一致的两个方法就可以共存,也就是说,两个方法如果有相同的名称和特征签名(参数是带泛型的类型擦除后特征签名一样),但返回值不同,那么他们可以合法共存在一个Class文件中 ## 糖块三、 自动装箱与拆箱 ## 自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。 先来看个自动装箱的代码: public static void main(String[] args) { int i = 10; Integer n = i } 反编译后代码如下: public static void main(String args[]){ int i = 10; Integer n = Integer.valueOf(i); } 从反编译得到内容可以看出,在装箱的时候**自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法**。 所以,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。 **扩展:**[拆箱装箱--一篇全了解:https://blog.csdn.net/w372426096/article/details/81909792][--_https_blog.csdn.net_w372426096_article_details_81909792] ## 糖块四 、 方法变长参数 ## 可变参数(variable arguments)是在Java 1.5中引入的一个特性,它允许一个方法把任意数量的值作为参数。 看下以下可变参数代码,其中print方法接收可变参数: public static void main(String[] args) { print("Franco", "WQ:Franco", "博客:https://blog.csdn.net/w372426096"); } public static void print(String... strs){ for (int i = 0; i < strs.length; i++) { System.out.println(strs[i]); } } 反编译后代码: public static void main(String args[]){ print(new String[] { "Franco", "WQ:Franco", "\u535A\u5BA2\uFF1Ahttps://blog.csdn.net/w372426096" }); } public static transient void print(String strs[]){ for(int i = 0; i < strs.length; i++) System.out.println(strs[i]); } 从反编译后代码可以看出,可**变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中**。 ## 糖块五 、 枚举 ## Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,**enum就和class一样,只是一个关键字,他并不是一个类**,那么枚举是由什么类维护的呢,我们简单的写一个枚举:public enum t \{ SPRING,SUMMER;\}然后我们使用反编译,看看这段代码到底是怎么实现。通过反编译后代码我们可以看到(内容太多不贴了,读者自行反编译了解下),public final class T extends Enum,说明,该类是**继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的**。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。 ## 糖块六 、 内部类 ## 内部类又称为嵌套类,可以把**内部类理解为外部类的一个普通成员**。 内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,**一旦编译成功,就会生成两个完全不同的.class文件**了,分别是**outer.class和outer$inner.class**。所以内部类的名字完全可以和它的外部类名字相同。 ## 糖块七 、条件编译 ## —般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。如在C或CPP中,可以通过预处理语句来实现条件编译。 其实在Java中也可实现条件编译。我们先来看一段代码: public class ConditionalCompilation { public static void main(String[] args) { final boolean DEBUG = true; if(DEBUG) { System.out.println("Hello, DEBUG!"); } final boolean ONLINE = false; if(ONLINE){ System.out.println("Hello, ONLINE!"); } } } 反编译后代码如下: public class ConditionalCompilation{ public static void main(String args[]) { boolean DEBUG = true; System.out.println("Hello, DEBUG!"); boolean ONLINE = false; } } 首先,我们发现,在反编译后的代码中没有System.out.println("Hello, ONLINE!");,这其实就是条件编译。当if(ONLINE)为false的时候,编译器就没有对其内的代码进行编译。 所以,Java语法的条件编译,是**通过判断条件为常量的if语句实现**的。其原理也是Java语言的语法糖。根据if判断条件的真假,编译器直接把分支为false的代码块消除。通过该方式实现的条件编译,**必须在方法体内实现**,而**无法在正整个Java类的结构或者类的属性上进行条件编译**,这与C/C++的条件编译相比,确实更有局限性。在Java语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。 ## 糖块八 、 断言 ## 在Java中,assert关键字是从 Java SE 1.4 引入的,为了避免和老版本的Java代码中使用了assert关键字导致错误,Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关-enableassertions或-ea来开启。 看一段包含断言的代码: public class AssertTest { public static void main(String args[]) { int a = 1; int b = 1; assert a == b; System.out.println("外号:Franco"); assert a != b : "Franco"; System.out.println("博客:https://blog.csdn.net/w372426096"); } } 反编译后代码太多,不贴了,但是很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了assert这个语法糖我们节省了很多代码。其实**断言的底层实现就是if语言,如果断言结果为true,则什么都不做,程序继续执行,如果断言结果为false,则程序抛出AssertError来打断程序的执行。-enableassertions会设置$assertionsDisabled字段的值。** ## 糖块九 、 数值字面量 ## 在Java 7中,数值字面量,不管是整数还是浮点数,**都允许在数字之间插入任意多个下划线**。 这些下划线不会对字面量的数值产生影响,目的就是方便阅读。 比如: public class Test { public static void main(String... args) { int i = 10_000; System.out.println(i); } } 反编译后: public class Test{ public static void main(String[] args) { int i = 10000; System.out.println(i); } } 反编译后就是把\_删除了。也就是说 编译器并不认识在数字字面量中的\_,需要在编译阶段把他去掉。 ## 糖块十 、 for-each ## 增强for循环(for-each)相信大家都不陌生,日常开发经常会用到的,他会比for循环要少写很多代码,那么这个语法糖背后是如何实现的呢? public static void main(String... args) { String[] strs = {"Franco", "WQ:Franco", "博客:https://blog.csdn.net/w372426096"}; for (String s : strs) { System.out.println(s); } } 反编译后代码如下: public static transient void main(String args[]){ String strs[] = { "Franco", "WQ1AFranco", "\u535A\u5BA2\uFF1Ahttps://blog.csdn.net/w372426096" }; String args1[] = strs; int i = args1.length; for(int j = 0; j < i; j++) { String s = args1[j]; System.out.println(s); } } 代码很简单,for-each的实现原理其实就是使用了**普通的for循环和迭代器。** ## 糖块十一 、 try-with-resource ## Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。 关闭资源的常用方式就是在finally块里是释放,即调用close方法。比如,我们经常会写这样的代码: public static void main(String[] args) { BufferedReader br = null; try { String line; br = new BufferedReader(new FileReader("d:\\Franco.xml")); while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { // handle exception } finally { try { if (br != null) { br.close(); } } catch (IOException ex) { // handle exception } } } 从Java 7开始,jdk提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下: public static void main(String... args) { try (BufferedReader br = new BufferedReader(new FileReader("d:\\ Franco.xml"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { // handle exception } } 看,这简直是一大福音啊,虽然我之前一般使用IOUtils去关闭流,并不会使用在finally中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。看下他的背后: public static transient void main(String args[]) { BufferedReader br; Throwable throwable; br = new BufferedReader(new FileReader("d:\\ Franco.xml")); throwable = null; String line; try { while((line = br.readLine()) != null) System.out.println(line); } catch(Throwable throwable2) { throwable = throwable2; throw throwable2; } if(br != null) if(throwable != null) try { br.close(); } catch(Throwable throwable1) { throwable.addSuppressed(throwable1); } else br.close(); break MISSING_BLOCK_LABEL_113; Exception exception; exception; if(br != null) if(throwable != null) try { br.close(); } catch(Throwable throwable3) { throwable.addSuppressed(throwable3); } else br.close(); throw exception; IOException ioexception; ioexception; } } 其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。 总结 前面介绍了12种Java中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成JVM认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。 参考: 《深入理解JVM虚拟机》 Hollis知识星球 [--_https_blog.csdn.net_w372426096_article_details_81909792]: https://blog.csdn.net/w372426096/article/details/81909792
还没有评论,来说两句吧...