语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。
我们所熟知的编程语言中几乎都有语法糖。作者认为,语法糖的多少是评判一个语言够不够牛逼的标准之一。很多人说Java是一个“低糖语言”,其实从Java 7开始Java语言层面上一直在添加各种糖,主要是在“Project Coin”项目下研发。尽管现在Java有人还是认为现在的Java是低糖,未来还会持续向着“高糖”的方向发展。
语法糖越多,更方便程序员的使用。但是我觉得还是增加了阅读成本。。使用语法糖,你总得认识这个糖🍬吧?
解语法糖 前面提到过,语法糖的存在主要是方便开发人员使用。但其实,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中的switch自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ackii码是整型)以及int。
那么接下来看下switch对String得支持,有以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class switchDemoString { public static void main (String[] args) { String str = "world" ; switch (str) { case "hello" : System.out.println("hello" ); break ; case "world" : System.out.println("world" ); break ; default : break ; } } }
反编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 ; case 113318802 : if (s.equals("world" )) System.out.println("world" ); break ; } } }
看到这个代码,你知道原来字符串的switch是通过equals()和hashCode()方法来实现的。 (hash值一样不代表对象一定相等,可能发生哈希碰撞。所以要再进一步通过equals比较。老问题了)
泛型 对于Java虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。
类型擦除的主要过程如下:
将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
移除所有的类型参数。
虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
具体参考上篇泛型类型擦除http://www.kylin.show/42613.html
自动装箱和拆箱 自动装箱就是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; }
反编译后代码如下:
1 2 3 4 5 public static void main (String args[]) { int i = 10 ; Integer n = Integer.valueOf(i); }
自动装箱Integer n = Integer.valueOf(i);实际调用了valueOf()方法
再来看个自动拆箱的代码:
1 2 3 4 5 public static void main (String[] args) { Integer i = 10 ; int n = i; }
反编译后代码如下:
1 2 3 4 5 6 public static void main (String args[]) { Integer i = Integer.valueOf(10 ); int n = i.intValue(); }
从反编译得到内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。
所以,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。
方法变长参数 可变参数(variable arguments)是在Java 1.5中引入的一个特性。它允许一个方法把任意数量的值作为参数。
看下以下可变参数代码,其中print方法接收可变参数:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { print("kylin" , "www.kylin.show" , "QQ:171346168" ); } public static void print (String... strs) { for (int i = 0 ; i < strs.length; i++) { System.out.println(strs[i]); } }
反编译后代码:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String args[]) { print(new String[] { "kylin" , "www.kylin.show" , "QQ:171346168" }); } public static transient void print (String strs[]) { for (int i = 0 ; i < strs.length; i++) System.out.println(strs[i]); }
从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。
枚举 Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。https://www.kylin.show/50392.html
要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:
1 2 3 public enum t { SPRING,SUMMER; }
使用反编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public final class T extends Enum { private T (String s, int i) { super (s, i); } public static T[] values() { T at[]; int i; T at1[]; System.arraycopy(at = ENUM$VALUES, 0 , at1 = new T[i = at.length], 0 , i); return at1; } public static T valueOf (String s) { return (T)Enum.valueOf(demo/T, s); } public static final T SPRING; public static final T SUMMER; private static final T ENUM$VALUES[]; static { SPRING = new T("SPRING" , 0 ); SUMMER = new T("SUMMER" , 1 ); ENUM$VALUES = (new T[] { SPRING, SUMMER }); } }
通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
内部类 内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。
内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。所以内部类的名字完全可以和它的外部类名字相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class OutterClass { private String userName; public String getUserName () { return userName; } public void setUserName (String userName) { this .userName = userName; } public static void main (String[] args) { } class InnerClass { private String name; public String getName () { return name; } public void setName (String name) { this .name = name; } } }
以上代码编译后会生成两个class文件:OutterClass$InnerClass.class 、OutterClass.class。当我们尝试对OutterClass.class文件进行反编译的时候,命令行会打印以下内容:Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad 。他会把两个文件全部进行反编译,然后一起生成一个OutterClass.jad文件。文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class OutterClass { class InnerClass { public String getName () { return name; } public void setName (String name) { this .name = name; } private String name; final OutterClass this $0 ; InnerClass() { this .this $0 = OutterClass.this ; super (); } } public OutterClass () { } public String getUserName () { return userName; } public void setUserName (String userName) { this .userName = userName; } public static void main (String args1[]) { } private String userName; }
条件编译 —般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。
如在C或CPP中,可以通过预处理语句来实现条件编译。其实在Java中也可实现条件编译。我们先来看一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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!" ); } } }
反编译后代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ConditionalCompilation { public 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 7中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。
比如:
1 2 3 4 5 6 public class Test { public static void main (String[] args) { int i = 10_000 ; System.out.println(i); } }
反编译后
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { int i = 10000 ; System.out.println(i); } }
反编译后就是把_删除了。也就是说 编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉。
foe-each 增强for循环(for-each)相信大家都不陌生,日常开发经常会用到的,他会比for循环要少写很多代码,那么这个语法糖背后是如何实现的呢?
1 2 3 4 5 6 7 8 9 10 public static void main (String... args) { String[] strs = {"kylin" , "www.kylin.show" , "QQ:171346168" }; for (String s : strs) { System.out.println(s); } List<String> strList = ImmutableList.of("kylin" , "www.kylin.show" , "QQ:171346168" ); for (String s : strList) { System.out.println(s); } }
反编译后代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static transient void main (String args[]) { String strs[] = { "kylin" , "www.kylin.show" , "QQ:171346168" }; String args1[] = strs; int i = args1.length; for (int j = 0 ; j < i; j++) { String s = args1[j]; System.out.println(s); } List strList = ImmutableList.of("kylin" , "www.kylin.show" , "QQ:171346168" ); String s; for (Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s)) s = (String)iterator.next(); }
代码很简单,for-each的实现原理其实就是使用了普通的for循环和迭代器。
try-with-resource Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。
关闭资源的常用方式就是在finally块里是释放,即调用close方法。比如,我们经常会写这样的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) { BufferedReader br = null ; try { String line; br = new BufferedReader(new FileReader("C:\\kylin.xml" )); while ((line = br.readLine()) != null ) { System.out.println(line); } } catch (IOException e) { } finally { try { if (br != null ) { br.close(); } } catch (IOException ex) { } } }
从Java 7开始,jdk提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { try (BufferedReader br = new BufferedReader(new FileReader("C:\\kylin.xml" ))) { String line; while ((line = br.readLine()) != null ) { System.out.println(line); } } catch (IOException e) { } }
反编译后查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static transient void main (String args[]) { BufferedReader br; Throwable throwable; br = new BufferedReader(new FileReader("d:\\ hollischuang.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; } }
额。。,其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。