1. 程式人生 > >【Java基礎】Java中的語法糖

【Java基礎】Java中的語法糖

目錄

  • Java中的語法糖
  • switch對String和列舉類的支援
  • 對泛型的支援
  • 包裝型別的自動裝箱和拆箱
  • 變長方法引數
  • 列舉
  • 內部類
  • 條件編譯
  • 斷言
  • 數值字面量
    • for-each
  • try-with-resource
  • Lambda表示式
    • Lambda表示式的語法
    • 基本的Lambda例子(實現功能介面)
    • 使用Lambdas排序集合
    • 使用Lambdas和Streams
  • 字串對+號的支援
  • 參考

語法糖(Syntactic Sugar),也稱糖衣語法,指在計算機語言中新增的某種語法,這種語法對語言本身功能來說沒有什麼影響,只是為了方便程式設計師的開發,提高開發效率。說白了,語法糖就是對現有語法的一個封裝。

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


Java中的語法糖

  • switch支援String和列舉
  • 泛型
  • 自動裝箱與拆箱
  • 方法變長引數
  • 列舉
  • 內部類
  • 條件編譯
  • 斷言
  • 數值字面量
  • for-each
  • try-with-resource
  • Lambda表示式
  • 字串+號。

switch對String和列舉類的支援

switch對列舉和String的支援原理其實差不多。switch原生支援只能支援比較整數型別。如果switch後面是String型別的話,編譯器會將其轉換成String的hashCode的值,所以其實比較的是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:
            if(s.equals("hello"))
                System.out.println("hello");
            break;
        case 113318802:
            if(s.equals("world"))
                System.out.println("world");
            break;
        }
    }
}

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

對泛型的支援

在JDK1.5中,Java語言引入了泛型機制。但是這種泛型機制是通過型別擦除來實現的,即Java中的泛型只在程式原始碼中有效(原始碼階段提供型別檢查),在編譯後的位元組碼中自動用強制型別轉換進行替代。也就是說,Java語言中的泛型機制其實就是一顆語法糖,相較與C++、C#相比,其泛型實現實在是不那麼優雅。

/**
* 在原始碼中存在泛型
*/
public static void main(String[] args) {
    Map<String,String> map = new HashMap<String,String>();
    map.put("hello","你好");
    String hello = map.get("hello");
    System.out.println(hello);
}

編譯後的程式碼

public static void main(String[] args) {
    HashMap map = new HashMap(); //型別擦除
    map.put("hello", "你好");
    String hello = (String)map.get("hello");//強制轉換
    System.out.println(hello);
}

通過上面反編譯後的程式碼我們發現虛擬機器中其實是沒有泛型的,只有普通類和普通方法,所有泛型類的型別引數在編譯時都會被擦除,泛型類並沒有自己獨有的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);
}

編譯後的程式碼

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

變長方法引數

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

列舉

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 SE 1.4 引入的,為了避免和老版本的Java程式碼中使用了assert關鍵字導致錯誤,Java在執行的時候預設是不啟動斷言檢查的(這個時候,所有的斷言語句都將忽略!)。

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

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

數值字面量

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

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

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

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

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

for-each

增強for迴圈的物件要麼是一個數組,要麼實現了Iterable介面。這個語法糖主要用來對陣列或者集合進行遍歷,其在迴圈過程中不能改變集合的大小。增強for迴圈主要使程式碼更加簡潔,其背後的原理是編譯器將增強for迴圈轉換成了普通的for迴圈。

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的拼接首先會呼叫 String.valueOf(obj),這個Obj為str1,而String.valueOf(Obj)中的實現是return obj == null ? “null” : obj.toString(), 然後產生StringBuilder, 呼叫的StringBuilder(str1)構造方法, 把StringBuilder初始化,長度為str1.length()+16,並且呼叫append(str1)! 接下來呼叫StringBuilder.append(str2), 把第二個字串拼接進去, 然後呼叫StringBuilder.toString返回結果!

所以答案是:StringBuilder(“null”).append(“abc”).toString(); //nullabc

參考

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