02-02-08、JDK8的语法糖

原创
2020/03/29 14:09
阅读数 78

​1、什么是语法糖

        如果要来进行一个拆字游戏的话,语法糖可以拆分成“语法” 和 “糖”,每一种计算机语言都有自己的语法,Java也不例外。

        语法就是在这一个语言中,要表示一种行为的表示模式,如赋值:int  i = 666;如循环while(foo){},语法在一种语言的运行环境中是原生支持的,是体现在其运行时当中的。

        糖是则是一种对于吃它的人来说感觉到甜的碳水化合物,往其他食物中加入糖会让食物变甜,吃糖能让人愉悦。

 

 

        语法糖(Syntactic Sugar)就是给语法裹上一层糖,因此语法糖也称语法糖衣,让使用这些被裹上糖的语法的编码人员感觉到“甜”。例如,Java中使用for循环语法对一个集合进行遍历,写法为:

int [] collection = new int[6];for(int i = 0; i < collection.length; i++){  // do something}

而现在,你只需要这么写就行:

int [] collection = new int[6];for(int i : collection){    // do something}

是否内心会感到小小的“甜”呢。

 

        有人会感觉很奇怪,我就是这么写的啊,这不就是Java的语法么?还说是什么语法糖!呸!~

 

        我们都知道,Java是一门高级语言,它的运行环境JVM执行的是字节码,也就是.java文件编译后得到的.class文件中的内容,而语法指的是JVM运行时所支持的表达模式。而语法糖能被用户所看见,当.java文件被编译成.class文件后,这一层糖衣将被剥去,在JVM运行时是看不到的。

        用English描述就是:Syntactic sugar is a mean which allows Java to provide new syntax without implementing that on JVM level. The syntactic syntax is converted into another more general or low level construct by the compiler which can be understood by JVM.

        例如,我们常写的如下代码

List foo = new ArrayList();for(Object i : foo){    // do something}int [] bar =  new int[0];for(int i : bar){    // do something}

反编译后为:

List foo = new ArrayList();Object var3;for(Iterator var2 = foo.iterator(); var2.hasNext(); var3 = var2.next()) {}int[] bar = new int[0];int[] var8 = bar;int var4 = bar.length;for(int var5 = 0; var5 < var4; ++var5) {    int var10000 = var8[var5];}

 

可以看出,for(item  :  collection){}  的写法最终是被编译器编译成以for(exp1;boolean; exp2) 的形式表示的写法,并没有在运行时增加对新的写法的支持,这就是语法糖的实现原理。

 

2、JDK8中有哪些语法糖

        Java中很早就出现了语法糖,这里介绍的我将其分为两类:类型相关  和  写法相关。

        其中,类型相关的有:泛型枚举内部类基本类型封装类的自动拆装箱

      写法相关的有:变长参数方法增强for循环switch支持String和Enumtry-with-resource数值字面量允许加下划线"_"lambda表达式

 

类型相关

01

 

泛型(伪泛型,通过类型擦除并强转实现)

        泛型的本质就是类型的参数化,java泛型中的类型只存在于源码中,在编译成字节码后它将被替换成原生类型,并且在相应的地方插入了强制类型转换代码,因此java的泛型实现其实是一种语法糖,称为类型擦除,使用这种方式实现的泛型称为伪泛型。

        如源码:

 Map<String,List> map = new HashMap<>(); List foo = map.get("haha"); // 编码的时候感觉这里获取到的就是List实例

        编译后将变成:

 Map<String, List> map = new HashMap(); List foo = (List)map.get("haha"); // 但实际是取到Object后强转得到List实例

 

        与java实现泛型的方式相对应的是如C#的泛型,它的泛型在运行期生成,有自己的虚方法表和类型数据,一个泛型就是一种数据类型,称为类型膨胀,使用这种方式实现的泛型称为真泛型。

 

        鉴于Java的泛型的这种实现方式,因此我们在使用泛型是要注意: 

  • 方法重载:

    如果一个方法是 ***** method1(List<String> list), 它的重载方法是 ***** method1(List<Integer> list),那么它们是编译不通过的,因为java的泛型是伪泛型,编译时会进行类型擦除,因此编译后它们都变成了 ***** method1(List  list),方法签名一样,因此编译不通过。

     

  • 泛型不能用在定义异常类上,例如定义了ExceptionA<T>,那么在catch(ExceptionA<String>) 和 catch(ExceptionA<Integer>)时,是无法分辨的。

     

  • 泛型类中不要设置静态变量(即类变量),因为类型擦除后实例关联的是同一个类,静态变量将被共享,会发生数据被篡改的问题。

 

02

枚举(实际编译成一个对应的类,这个类继承java.lang.Enum抽象类,类内部为对应枚举项数量的此类类型常量)

        枚举源于数学术语,就是列举出一个有限集。因此,Java中的枚举其实也就是一个同类型对象的有限集。如:

public enum EnumY {    A,L,E,X}

        这个集合中有4个元素,他们都是Enum类型的对象。经编译后:

public final class EnumY extends Enum{  // 注意,这里是个final类    public static EnumY[] values(){        return (EnumY[])$VALUES.clone(); // 这里获取的是一个clone后得到的数组    }    public static EnumY valueOf(String name){        return (EnumY)Enum.valueOf(com/gelicheng/syntax/enum1/EnumY, name);    }    private EnumY(String s, int i){        super(s, i);    }    public static final EnumY A;    public static final EnumY L;    public static final EnumY E;    public static final EnumY X;    private static final EnumY $VALUES[];    static {        A = new EnumY("A", 0);        L = new EnumY("L", 1);        E = new EnumY("E", 2);        X = new EnumY("X", 3);        $VALUES = (new EnumY[] {            A, L, E, X        });    }}

       复杂一点的枚举:

public enum  EnumX {    FOO("foo"){        public String say(){            return "NB foo";        }    },    BAR("bar"){        public String say(){            return "NB bar";        }    };    private EnumX(String good) {        this.good = good;    }    private String good;    private static String nice = "HNB";    public abstract String say();    public String getGood() {        return good;    }    public String getNice() {        return nice;    }}

        反编译后:

public abstract class EnumX extends Enum{ // 注意,这里是个abstract类    public static EnumX[] values(){        return (EnumX[])$VALUES.clone();    }    public static EnumX valueOf(String name){        return (EnumX)Enum.valueOf(com/gelicheng/syntax/enum1/EnumX, name);    }    private EnumX(String s, int i, String good){        super(s, i);        this.good = good;    }    public abstract String say();    public String getGood(){        return good;    }    public String getNice(){        return nice;    }    public static final EnumX FOO;    public static final EnumX BAR;    private String good;    private static String nice = "HNB";    private static final EnumX $VALUES[];    static {        FOO = new EnumX("FOO", 0, "foo") {            public String say(){                return "NB foo";            }        };        BAR = new EnumX("BAR", 1, "bar") {            public String say(){                return "NB bar";            }        };        $VALUES = (new EnumX[] {            FOO, BAR        });    }}

         因此,从枚举的实现中可以看出它的特性:

  • 枚举就是一个继承了Enum抽象类的类,因此它还可以实现其他接口,但不能再继承其他类(因为它默认继承了Enum,而Java是单继承);

  • 枚举类可以有自己的构造方法,方法,属性,静态属性;

  • 定义的枚举中可以有抽象方法,因为此时定义的枚举(也就是我们写的枚举类)实际相当于一个抽象类,最终编译后才会形成最终枚举元素的类实例;

  • 枚举类是单例的,因为我们无法通过new得到一个枚举类,而是XXEnum.yy直接使用的,它相当于一个特殊的集合,只会出现一个实例。编译后各个枚举项对象在其内部是final的,外部的枚举也是final的,故枚举的构造方法默认是private的,也只能是private的。这也就是为什么很多人用枚举实现单例模式的原因,而且它更安全,只能通过反序列化破坏它的单例状态。

  • 因为枚举类继承自Enum,因此枚举对象都有Enum的compareTo(E o)、valueOf(Class<T> enumType,String name)、toString()等方法(注:toString()方法在Enum中的实现返回的是name)。

         需要注意的是,定义的静态属性是属于外部的类的,而非每一个枚举元素对象的,因此在枚举中定义静态属性一定要小心。

 

03

内部

内部类(实际编译出对应的内部类的.class,类名一般为xxx$yyyy.class)

        内部类其实比较简单,就是将定义在内部的类编译成一个xxx$yyy.class文件,如:

        内部类就相当于其外层类的一个属性类,内部类中可以直接调用外层类的方法和属性;在其他类中也可以通过像访问属性一下访问到内部类;同时,内部类不会跟随外层类实例化而实例化,它是在使用到时才实例化,这也是为什么使用内部类实现懒加载的单例模式的原因。

 

04

基本类型封装类自动拆装

自动拆箱与装箱(实际使用的是XX.valueOf(param)方法或xxValue()方法)

        什么是自动拆箱、装箱?Java是一种主打面向对象的语言,在Java中,一切都是对象,但出于性能考虑(为什么?自己想想),保留了基本类型(byte、short、int、long、float、double、char、boolean)。但对于标榜一切皆对象的Java来说,基本类型不是对象,于是乎便对基本类型进行了封装,使基本类型也能变成对象。

 

        当程序中,将要对一个封装类和一个基本类型相互操作时,就需要将一种形式转换成另一种目标形式,同类型的才能相互操作。如:

public static void main(String [] args){   int i = new Integer(0);  // 需要转成基本类型,才能赋值给基本类型 i   Integer j = 6;  // 需要转换成封装类型,才能赋值给封装类型 j   boolean b = i == j; // 需要拆箱成基本类型   boolean b2 = j == i;  // 需要拆箱成基本类型   boolean b3 = j.equals(i); // 需要装箱成封装类型}

        编译后:

public static void main(String args[]){   int i = (new Integer(0)).intValue(); // 使用.intValue()实现拆箱   Integer j = Integer.valueOf(6); // 使用.valueOf()实现装箱   boolean b1 = i == j.intValue(); // 使用.intValue()拆箱成基本类型才能用==比较   boolean b2 = j.intValue() == i; // 使用.intValue()拆箱成基本类型才能用==比较   boolean b3 = j.equals(Integer.valueOf(i)); // 使用.valueOf()实现装箱才能使用equals比较}

        从自动拆箱和装箱的实现方式可以知道:

  • 一个基本类型和一个封装类型操作可以实现自动拆装箱;

  • 一个基本类型和一个封装类型判等时可以直接使用 “==”方式比较,而不用担心出现问题,因为会自动拆箱成基本类型;

  • 如果是两个封装类型,请使用equals比较,不要使用“==”,因为两个封装类型不需要拆装箱。

    (注:虽然有些情况“==”会相等,如:Integer foo = 66; Integer bar = 66; boolean b = foo == bar; // true,但这个是因为Integer使用了享元模式,而非因为其自动拆装箱,所以切记:两个封装类型比较,请使用equals比较,不要使用“==”)

 

 

写法相关

 

05

变长参数方

变长参数方法(将对应的变长参数转为数组)

        变长参数方法会在编译后,将变长的部分转成对应的数组,如:

public void foo(boolean isFoo, String... items) { // 变长参数   boolean b = null == items && isFoo;}

        编译后:

public transient void foo(boolean isFoo, String items[])// 变长部分被转成了数组   boolean b = null == items && isFoo;}

        需要注意的是,变长参数只能是方法的最后一个参数。

 

 

06

增强for循环

增强for循环(基于原有的for、while语法,编译后使用数组或迭代器实现for循环)

        源码如:

int [] array = new int[3];List list = new ArrayList();for(int i : array){    System.out.println(i);}for(Object obj : list){    System.out.println(obj);}

        编译后:

int[] array = new int[3];List list = new ArrayList();int[] var3 = array;int var4 = array.length;for(int var5 = 0; var5 < var4; ++var5) {    int i = var3[var5];    System.out.println(i);}Iterator var7 = list.iterator();while(var7.hasNext()) {    Object obj = var7.next();    System.out.println(obj);}

        从增强for循环的实现可以看出,它没有实现对集合为null时的处理,因此遍历前要注意对null的处理。

 

 

07

switch支持String和枚

switch支持String和Enum(实际是将对应的case值转换为对应的hashCode,将switch参数取hash值进行匹配,然后用equals方法进行安全检查)

        switch原本只支持几种基本类型(char、byte、shor、int及其对应的封装类--自动拆箱),但在JDK7后,支持了String和Enum,它是怎么实现的呢?

        源码如下:

String l = "foo";switch (l){    case "":  // case 为空字符串“”        System.out.println("null str");        return;    case "bar": // case 为字符串“bar”        System.out.println("bar");        return;    default:        System.out.println("other");        return;}

        编译后:

String l = "foo";byte var3 = -1;switch(l.hashCode()) {  // 取string的hashCode作为switch参数  case 0:  // case 直接变成“”的hashCode, 空字符串的hashCode是0    if (l.equals("")) {        var3 = 0;  // 把case指为 case 0,如果有多个case,会指为1、2、……    }    break;  case 97299:  // case 直接变成“bar”的hashCode    if (l.equals("bar")) {  // 使用equals进行检查,以免出现hash碰撞出现问题        var3 = 1; // 把case指为 case 1    }}switch(var3) {  case 0: // 执行case 0 对应的逻辑,但需要注意的是这个 0 不是空字符串的 hashCode的0    System.out.println("null str");    return;  case 1: // 执行case 1 对应的逻辑    System.out.println("bar");    return;  default:    System.out.println("other");}

        支持Enum的也跟String一样,也是通过两个switch,使用hashCode和equals实现。

 

 

08

try-with-resourc

try-with-resource(自动在try中新增了一个try-catch-finally,并在此finally中执行AutoCloaseable的close()方法)

        源码:

// 注意 AutoCloaseable 实例必须在try()括号中实例化,不然是编译不通过的try (FileInputStream fs = new FileInputStream(new File("alex.hello"))) {    System.out.println("oh yeah");} catch (FileNotFoundException | TransactionRequiredException e) {    System.out.println("oh god");}

        编译后代码:

try {    FileInputStream fs = new FileInputStream(new File("alex.hello"));    Throwable var2 = null;    try {        System.out.println("oh yeah");    } catch (Throwable var12) {        var2 = var12;        throw var12;    } finally {        if (fs != null) {            if (var2 != null) {                try {                    fs.close();                } catch (Throwable var11) {                    var2.addSuppressed(var11);                }            } else {                fs.close();            }        }    }} catch (TransactionRequiredException | FileNotFoundException var14) {    System.out.println("oh god");}

        从try-with-resource的实现可以知道:

  • AutoCloaseable资源实例的作用域是在try{}块中的,对应的就是源码中AutoCloaseable 实例必须在try()括号中实例化,不然自动关闭无从实现,这也是为什么需要这么写的原因。

 

 

09

数值字面量允许加下划线"_"

数值字面量允许加下划线"_"(如 int  i = 999_999_999.00;  double d = 999_999.990_99;  对数值没影响,只是方便读,编译后变成原来的数值)

        这个语法糖没有特别的地方,只是为了人方便读。我们经常会看到在显示一个比较长的数字时,会有从小数点开始每3位加一个逗号(','),如:9,527.00,方便快速识别这是一个多大量级的数。Java中的这个语法糖就允许在源码编写时加入下划线('_')分隔符,然后在编译时将其擦除。如:

        源码:

short s = 123_4;int i = 1990_11_10;long l = 123_456_789_0L;double d = 678_952_7.000_666;

        编译后:

short s = 1234;int i = 19901110;long l = 1234567890L;double d = 6789527.000666;

        这个就没什么特别需要注意的了。只是.....你是不是现在才发现有这个

 

10

lambda表达式

lambda表达式(在编译后会转换成调用对应一些lambda API)

        这个是否是语法糖,大家尚有一些小小的不同的看法。首先可以确定的是,lambda表达式不是接口匿名实现类的语法糖,它是JDK8在JVM层面基于动态指令支持的。

 

3、结语

        语法糖在圈子里是有一些争议的,主要在两方面,一方面是:该不该有语法糖这个东西,另一方面是:一个语法是不是另一种语法的语法糖。

        对于该不该有语法糖,各有其道理。有的说语法糖破坏了统一性,有的说语法糖提高了代码的表述能力……  这不是我们要讨论的。

 

        对于某一个语法是不是另一个语法的语法糖的争论也不少,如Java中:

x += 1 ;   是不是   x = x + 1;  的语法糖?(注:有说法是,一般情况下这两个是等价的,但如果加了volatile语义会不一样,所以前者不是后者的语法糖。 有点深奥哦,谁想明白了记得留言告诉我
        对于是不是语法糖的争议主要是角度不同,也是各有其道理。

 

        如一类观点是“Syntactic sugar is a mean which allows Java to provide new syntax without implementing that on JVM level. The syntactic syntax is converted into another more general or low level construct by the compiler which can be understood by JVM”,这是从运行环境去看的。

        还有一种观点是“A construct in a language is called "syntactic sugar" if it can be removed from the language without any effect on what the language can do: functionality and expressive power will remain the same.”,这个从语言本身去看的。

 

        从不同的定义层次会发现还有不少的语法糖,但有些不怎么用,在此不太过多罗列。本文的焦点都不在于找到多少语法糖,本文只想通过语法糖,说说Java中一些语法或功能是如何实现的,这对我们有益的。

 

写的不好,请多见谅,希望不误人子弟,如有不对的地方还望不吝指正,谢谢!


 

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部