语法糖 语法糖(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;     } } 
额。。,其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。