Java中的語法糖



語法糖(Syntactic Sugar),也稱糖衣語法,指在計算機語言中添加的某種語法,這種語法對語言本身的功能來說沒有什么影響,只是為了方便程序員進行開發,提高開發效率,使用這種語法寫出來的程序可讀性也更高。說白了,語法糖就是對現有語法的一個封裝。

但其實,Java虛擬機是並不支持語法糖的,語法糖在程序編譯階段就會被還原成簡單的基礎語法結構,這個過程就是解語法糖。所以在Java中真正支持語法糖的是Java編譯器。


Java中的語法糖

Java編程語言提供了很多語法糖,整理了下,主要有下面幾種常用的語法糖。

  • switch-case對String和枚舉類的支持
  • 泛型
  • 包裝類自動裝箱與拆箱
  • 方法變長參數
  • 枚舉
  • 內部類
  • 條件編譯
  • 斷言
  • 數值字面量
  • 增強for循環
  • try-with-resource語法
  • Lambda表達式
  • 字符串+號語法

switch對String和枚舉類的支持

switch對枚舉和String的支持原理其實是差不多的。下面以String為列子介紹下具體的原理。

switch關鍵字原生只能支持整數類型。如果switch后面是String類型的話,編譯器會將其轉換成String的hashCode的值,所以其實switch-case語法比較的是String的hashCode值。

如果switch后面是Enum類型的話,編譯器會將其轉換為這個枚舉定義的下標(ordinal)。其實最后都是比較的整數類型。下面以Stirng舉個列子。

源代碼

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;
        }
    }
}

反編譯后的代碼

public class SwitchDemoString
{
    public switchDemoString()
    {
    }
    public static void main(String args[])
    {
        String str = "world";
        String s;
        switch((s = str).hashCode())
        {
        default:
            break;
        case 99162322:
            //這邊需要再次通過equals方法進行判斷,因為不同字符串的hashCode值是可能相同的,比如“Aa”和“BB”的hashCode就是一樣的
            if(s.equals("hello"))
                System.out.println("hello");
            break;
        case 113318802:
            if(s.equals("world"))
                System.out.println("world");
            break;
        }
    }
}

通過反編譯可以發現,進行switch的實際是哈希值,然后通過使用equals方法比較進行安全檢查,這個檢查是必要的,因為哈希可能會發生碰撞。因此它的性能是不如使用枚舉進行switch或者使用純整數常量。

PS:這邊順帶簡單介紹一個Java的反編譯軟件Jad。下載地址的話大家可以在網上找下。使用起來非常簡單:

jad -p SwitchDemoString.class > SwitchDemoString.java

上面的命令就可以將class文件反編譯成Java文件。這個只是Jad的最簡單的使用,詳細用法可以參考這篇博客

對泛型的支持

在JDK5中,Java語言引入了泛型機制。但是這種泛型機制其實是通過類型擦除來實現的,即Java中的泛型只在程序源代碼中有效(源代碼階段提供類型檢查),在編譯后的字節碼中自動用強制類型轉換進行替代。也就是說,Java語言中的泛型機制其實就是一顆語法糖,相較與C++、C#相比,其泛型實現實在是不那么優雅。

/**
* 在源代碼中存在泛型
*/
public static void main(String[] args) {
    Map<String,String> map = new HashMap<String,String>(16);
    map.put("name","csx-mg");
    String name = map.get("name");
    System.out.println(name);
}

通過jad反編譯出來的代碼

public static void main(String[] args) {
    //類型擦除
    Map map = new HashMap();
    map.put("name", "csx-mg");
    //強制轉換
    String name = (String)map.get("name");
    System.out.println(name);
}

通過上面反編譯后的代碼我們發現虛擬機中其實是沒有泛型的,只有普通類和普通方法,所有泛型類的類型參數在編譯時都會被擦除,泛型類並沒有自己獨有的Class類對象。

包裝類型的自動裝箱和拆箱

我們知道在 Java 中的8個基本類型和對應的包裝類型之間是可以互相賦值的(這個過程叫自動裝箱、拆箱過程)。其實這背后的原理是編譯器做了優化。如下面代碼,將基本類型賦值給包裝類其實是調用了包裝類的valueOf()方法創建了一個包裝類再賦值給了基本類型。而包裝類賦值給基本類型就是調用了包裝類的xxxValue()方法拿到基本數據類型后再賦值的。

public static void main(String[] args) {
    Integer a = 1;
    int b = 2;
    int c = a + b;
    System.out.println(c);
}

通過jad反編譯出來的代碼

public static void main(String[] args) {
    // 自動裝箱
    Integer a = Integer.valueOf(1); 
    byte b = 2;
    //自動拆箱
    int c = a.intValue() + b;
    System.out.println(c);
}

變長方法參數

變長參數特性是在JDK 5中引入的,使用變長參數有兩個條件,一是變長的那一部分參數具有相同的類型,二是變長參數必須位於方法參數列表的最后面。變長參數同樣是Java中的語法糖,其內部實現是編譯器在編譯源代碼的時候將變長參數部分轉換成了Java數組。

public class Test {

    public static void main(String[] args) {
        m1("csx-mg","reading","writing","fishing");
    }

    public static void m1(String name,String... hobbits){
        System.out.println("l am "+name);
        System.out.print("l have hobbits:[");
        for (int i = 0; i < hobbits.length; i++) {
            if(i!=hobbits.length-1) {
                System.out.print(hobbits[i]+",");
            }else {
                System.out.print(hobbits[i]+"]");
            }
        }
    }
}

上面代碼輸出:

l am csx-mg
l have hobbits:[reading,writing,fishing]

通過jad反編譯出來的代碼

public class Test
{

    public Test()
    {
    }

    public static void main(String args[])
    {
        //入參也被轉成成了數組
        m1("csx-mg", new String[] {
            "reading", "writing", "fishing"
        });
    }
    
    //這邊已經將變長參數轉換成了數組
    public static transient void m1(String name, String hobbits[])
    {
        System.out.println((new StringBuilder()).append("l am ").append(name).toString());
        System.out.print("l have hobbits:[");
        for(int i = 0; i < hobbits.length; i++)
            if(i != hobbits.length - 1)
                System.out.print((new StringBuilder()).append(hobbits[i]).append(",").toString());
            else
                System.out.print((new StringBuilder()).append(hobbits[i]).append("]").toString());

    }
}

枚舉

java中類的定義使用class,枚舉類的定義使用enum。但在Java的字節碼結構中,其實並沒有枚舉類型,枚舉類型只是一個語法糖,在編譯完成后就會被編譯成一個普通的類,也是用Class修飾。這個類繼承java.lang.Enum,並被final關鍵字修飾。

public enum Fruit {
    APPLE,ORINGE
}	

將Fruit的class文件進行反編譯

//繼承java.lang.Enum並聲明為final
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)
    {
        super(s, i);
    }
    //枚舉類型常量
    public static final Fruit APPLE;
    public static final Fruit ORANGE;
    private static final Fruit $VALUES[];//使用數組進行維護

    static
    {
        APPLE = new Fruit("APPLE", 0);
        ORANGE = new Fruit("ORANGE", 1);
        $VALUES = (new Fruit[] {
            APPLE, ORANGE
        });
    }
}

通過上面反編譯的代碼,我們可以知道當我們使用enmu來定義一個枚舉類型的時候,編譯器會自動幫我們創建一個final類型的類繼承Enum類,所以枚舉類型不能被繼承。

內部類

Java語言中之所以引入內部類,是因為有些時候一個類只想在一個類中有用,我們不想讓其在另外一個地方被使用。內部類之所以是語法糖,是因為其只是一個編譯時的概念,一旦編譯完成,編譯器就會為內部類生成一個單獨的class文件,名為outer$innter.class。

public class Outer {
    class Inner{
    }
}

使用javac編譯后,生成兩個class文件Outer.class和Outer$Inner.class,其中Outer$Inner.class的內容如下:

class Outer$Inner {
    Outer$Inner(Outer var1) {
        this.this$0 = var1;
    }
}

條件編譯

一般情況下,源程序中所有的行都參加編譯。但有時希望對其中一部分內容只在滿足一定條件下才進行編譯,即對一部分內容指定編譯條件,這就是“條件編譯”(conditional compile)。

Java中的條件編譯是通過編譯器的優化原則實現的:

  • 如果if的條件是false,則在編譯時忽略這個if語句。
  • 忽略未使用的變量。
public class ConditionalCompilation02
{
    public static void main(String[] args) {
        if(CompilationConfig.DEBUG_MODE)
        {
            System.out.println("[DEBUG MODE]You can print sth");
        }
        else
        {
            System.out.println("[RELEASE MODE]You can print sth");
        }
    }
}

所以,Java語法的條件編譯,是通過判斷條件為常量的if語句實現的。根據if判斷條件的真假,編譯器直接把分支為false的代碼塊消除。通過該方式實現的條件編譯,必須在方法體內實現,而無法在正整個Java類的結構或者類的屬性上進行條件編譯。

斷言

在Java中,assert關鍵字是從Java 4開始引入的,為了避免和老版本的Java代碼中使用了assert關鍵字導致錯誤,Java在執行的時候默認是不啟動斷言檢查的(這個時候,所有的斷言語句都將忽略!)。

如果要開啟斷言檢查,則需要用開關-enableassertions或-ea來開啟。

其實斷言的底層實現就是if語言,如果斷言結果為true,則什么都不做,程序繼續執行,如果斷言結果為false,則程序拋出AssertError來打斷程序的執行。

public class Test {
    public static void main(String[] args) {
        System.out.println("begin to test assert...");
        boolean flag;
        //當布爾值為true的時候不會拋出錯誤
        flag = true;
        assert flag;
        //這邊會拋出AssertionError錯誤,但是需要加入vm參數-ea
        flag = false;
        assert flag;
    }
}

代碼輸出

begin to test assert...
Exception in thread "main" java.lang.AssertionError
	at com.csx.demo.spring.boot.utildemo.Test.main(Test.java:8)

通過jad反編譯出來的代碼

public class Test
{

    public Test()
    {
    }

    public static void main(String args[])
    {
        System.out.println("begin to test assert...");
        boolean flag = true;
        if(!$assertionsDisabled && !flag)
            throw new AssertionError();
        flag = false;
        if(!$assertionsDisabled && !flag)
            throw new AssertionError();
        else
            return;
    }

    static final boolean $assertionsDisabled = !com/csx/demo/spring/boot/utildemo/Test.desiredAssertionStatus();

}

通過反編譯后的代碼可以看出assert斷言就是通過對布爾標志位進行了一個if判斷。(需要注意的是如果需要使用斷言,需要設置JVM參數-ea開始斷言)

數值字面量

Java中支持如下形式的數值字面量

  • 十進制:默認的
  • 八進制:整數之前加數字0來表示
  • 十六進制:整數之前加"0x"或"0X"
  • 二進制(新加的):整數之前加"0b"或"0B"

另外在在jdk7中,數值字面量,不管是整數還是浮點數,都允許在數字之間插入任意多個下划線。這些下划線不會對字面量的數值產生影響,目的就是方便閱讀。比如:

  • 1_500_000
  • 5_6.3_4
  • 89_3___1

下划線只能出現在數字中間,前后必須是數字。所以“_100”、“0b_101“是不合法的,無法通過編譯。這樣限制的動機就是可以降低實現的復雜度。有了這個限制,Java編譯器只需在掃描源代碼的時候將所發現的數字中間的下划線直接刪除就可以了。如果不添加這個限制,編譯器需要進行語法分析才能做出判斷。比如:_100,可能是一個整數字面量100,也可能是一個變量名稱。這就要求編譯器的實現做出更復雜的改動。

public class Test {
    public static void main(String[] args) {
        //十進制
        int a = 10;
        //二進制
        int b = 0B1010;
        //八進制
        int c = 012;
        //十六進制
        int d = 0XA;

        double e = 12_234_234.23;
        System.out.println("a:"+a);
        System.out.println("b:"+b);
        System.out.println("c:"+c);
        System.out.println("d:"+d);
        System.out.println("e:"+e);
    }
}

通過jad反編譯出來的代碼

public class Test
{

    public Test()
    {
    }

    public static void main(String args[])
    {
        int a = 10;
        //編譯器已經將二進制,八進制,十六進制數轉換成了10進制數
        int b = 10;
        int c = 10;
        int d = 10;
        //編譯器已經將下滑線刪除
        double e = 12234234.23D;
        System.out.println((new StringBuilder()).append("a\uFF1A").append(a).toString());
        System.out.println((new StringBuilder()).append("b\uFF1A").append(b).toString());
        System.out.println((new StringBuilder()).append("c\uFF1A").append(c).toString());
        System.out.println((new StringBuilder()).append("d\uFF1A").append(d).toString());
        System.out.println((new StringBuilder()).append("e\uFF1A").append(e).toString());
    }
}

增強for循環

增強for循環的對象要么是一個數組,要么實現了Iterable接口。這個語法糖主要用來對數組或者集合進行遍歷,其在循環過程中不能改變集合的大小。增強for循環主要使代碼更加簡潔,其背后的原理是編譯器將增強for循環轉換成了普通的for循環或者while循環。

public static void main(String[] args) {
    String[] params = new String[]{"hello","world"};
    //增強for循環對象為數組
    for(String str : params){
        System.out.println(str);
    }

    List<String> lists = Arrays.asList("hello","world");
    //增強for循環對象實現Iterable接口
    for(String str : lists){
        System.out.println(str);
    }
}

編譯器編譯后的代碼

public static void main(String[] args) {
   String[] params = new String[]{"hello", "world"};
   String[] lists = params;
   int var3 = params.length;
   //數組形式的增強for退化為普通for
   for(int str = 0; str < var3; ++str) {
       String str1 = lists[str];
       System.out.println(str1);
   }

   List var6 = Arrays.asList(new String[]{"hello", "world"});
   Iterator var7 = var6.iterator();
   //實現Iterable接口的增強for使用iterator接口進行遍歷
   while(var7.hasNext()) {
       String var8 = (String)var7.next();
       System.out.println(var8);
   }

}

try-with-resource語法

當一個外部資源的句柄對象實現了AutoCloseable接口,JDK7中便可以利用try-with-resource語法更優雅的關閉資源,消除板式代碼。

public static void main(String[] args) {
    try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

將外部資源的句柄對象的創建放在try關鍵字后面的括號中,當這個try-catch代碼塊執行完畢后,Java會確保外部資源的close方法被調用。代碼是不是瞬間簡潔許多!try-with-resource並不是JVM虛擬機的新增功能,只是JDK實現了一個語法糖,當你將上面代碼反編譯后會發現,其實對JVM虛擬機而言,它看到的依然是之前的寫法:

public static void main(String[] args) {
    try {
        FileInputStream inputStream = new FileInputStream(new File("test"));
        Throwable var2 = null;
        try {
            System.out.println(inputStream.read());
        } catch (Throwable var12) {
            var2 = var12;
            throw var12;
        } finally {
            if (inputStream != null) {
                if (var2 != null) {
                    try {
                        inputStream.close();
                    } catch (Throwable var11) {
                        var2.addSuppressed(var11);
                    }
                } else {
                    inputStream.close();
                }
            }
        }

    } catch (IOException var14) {
        throw new RuntimeException(var14.getMessage(), var14);
    }
}

其實背后的原理也很簡單,那些我們沒有做的關閉資源的操作,編譯器都幫我們做了。

Lambda表達式

Lambda表達式雖然看着很先進,但其實Lambda表達式的本質只是一個"語法糖",由編譯器推斷並幫你轉換包裝為常規的代碼,因此你可以使用更少的代碼來實現同樣的功能。本人建議不要亂用,因為這就和某些很高級的黑客寫的代碼一樣,簡潔,難懂,難以調試,維護人員想罵娘

lambda表達式允許你通過表達式來代替功能接口。Lambda表達式還增強了集合庫。 Java SE 8添加了2個對集合數據進行批量操作的包: java.util.function 包以及java.util.stream 包。 流(stream)就如同迭代器(iterator),但附加了許多額外的功能。 總的來說,lambda表達式和 stream 是自Java語言添加泛型(Generics)和注解(annotation)以來最大的變化。

Lambda表達式的語法

基本語法:
(parameters) -> expression
或
(parameters) ->{ statements; }

Lambda表達式的一些簡單列子

// 1. 不需要參數,返回值為 5  
() -> 5  
  
// 2. 接收一個參數(數字類型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2個參數(數字),並返回他們的差值  
(x, y) -> x – y  
  
// 4. 接收2個int型整數,返回他們的和  
(int x, int y) -> x + y  
  
// 5. 接受一個 string 對象,並在控制台打印,不返回任何值(看起來像是返回void)  
(String s) -> System.out.print(s)

基本的Lambda例子(實現功能接口)

String[] atp = {"Rafael Nadal", "Novak Djokovic",
                "Stanislas Wawrinka",
                "David Ferrer", "Roger Federer",
                "Andy Murray", "Tomas Berdych",
                "Juan Martin Del Potro"};

        List<String> players =  Arrays.asList(atp);

        //實現功能接口
        players.forEach((String player) ->{
            System.out.println(player);
        });
        Runnable runnable = () -> {
            System.out.println("l am a new thread...");
        };
        new Thread(runnable).start();   

使用Lambdas排序集合

players.sort(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.compareTo(o2);
            }
        });
Comparator<String> comparator = (String o1,String o2) ->{return o1.compareTo(o2);};
players.sort(comparator);

使用Lambdas和Streams

Stream是對集合的包裝,通常和lambda一起使用。 使用lambdas可以支持許多操作,如 map, filter, limit, sorted, count, min, max, sum, collect 等等。 同樣,Stream使用懶運算,他們並不會真正地讀取所有數據,遇到像getFirst() 這樣的方法就會結束鏈式語法。

字符串對+號的支持

String s=null;
s=s+"abc";
System.out.println(s);

上面的代碼輸出什么?

字符串+號拼接原理:運行時,兩個字符串str1, str2的拼接首先會new一個StringBuilder對象,然后分別對字符串進行append操作,最后調用toString()方法。

看下反編譯出來的代碼:

public static void main(String args[])
{
    String s = null;
    s = (new StringBuilder()).append(s).append("abc").toString();
    System.out.println(s);
}

所以答案是:nullabc

參考

https://www.cnblogs.com/wanshiming/p/7685684.html
https://www.cnblogs.com/franson-2016/p/5593080.html
https://www.jb51.net/article/138519.htm

公眾號推薦

歡迎大家關注我的微信公眾號「程序員自由之路」


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM