Java編譯期處理(語法糖)—— 編譯器到底幹了 乾了些什麼?

2020-08-16 17:06:14

編譯期處理  ——  編譯器到底幹了 乾了些什麼?

目錄

編譯期處理

1.1 語法糖 - 預設構造器

1.2 語法糖 - 自動拆裝箱

1.3 語法糖 - 泛型擦除

1.4 語法糖 - 可變參數

1.5 語法糖 - foreach 回圈

1.6 語法糖 - switch-string字串

1.7 語法糖 - switch 列舉

1.8 語法糖 - 列舉類

1.9 語法糖 - try-with-resources

1.10 語法糖 - 重寫橋接

1.11 語法糖 - 匿名內部類


所謂的語法糖,其實就是指 java 編譯器所有  *.java 原始碼編譯爲  *.class 位元組碼的過程,自動生成和轉換的一些程式碼, 主要是爲了減輕程式設計師的負擔,算是 java 編譯器給我們的一個額外福利(給糖吃嘛)。

 

注意:  以下程式碼的分析,藉助了 javap 工具,idea 的反編譯功能,idea外掛 jclasslib等工具。另外,編譯器轉換的結果直接就是 class位元組碼,只是爲了便於閱讀,給出了幾乎等價的 java原始碼方式,並不是編譯器還會轉換出中間的 java原始碼,切記。


1.1 語法糖 - 預設構造器

先來看一下範例程式碼,裏面沒有自己宣告構造方法:

// 編譯期處理(語法糖)——預設構造器
public class T01_CompileTime_DefaultConstructor {
    
}

編譯成 class 後的程式碼: 裏面沒有宣告構造方法,無參構造是編譯器幫助我們加上的。即呼叫父類別 Object的無參構造方法:java/lang/Object."<init>"方法

// 編譯期處理(語法糖)——預設構造器
public class T01_CompileTime_DefaultConstructor {
    // 這個無參構造是編譯器幫助我們加止的
    public T01_CompileTime_DefaultConstructor() {
        super(); // 即呼叫父類別 Object 的無參構造方法,即呼叫 java/lang/Object."<init>":()V
    }
}

總結:

  1. 如果一個類中沒有宣告構造方法,無參構造是編譯器會幫我們加上的。即呼叫父類別 Object的無參構造方法:java/lang/Object."<init>"方法 

1.2 語法糖 - 自動拆裝箱

這個自動拆裝箱特性是 JDK 5 開始加入的,代段片段1如下:

// 編譯期處理(語法糖)——自動拆裝箱
public class T02_CompileTime_AutoPackUnpack {
    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}

這段程式碼在 JDK 5 之前無法編譯通過的,必須改寫爲 程式碼片段2:

// 編譯期處理(語法糖)——自動拆裝箱
public class T02_CompileTime_AutoPackUnpack {
    public static void main(String[] args) {
        Integer x = Integer.valueOf(1); // 裝箱
        int y = x.intValue; // 拆箱
    }
}

顯然之前版本的程式碼太麻煩了,需要在基本型別和包裝型別之間來回轉換(尤其是集合類中操作的都是包裝型別),因此這些轉換的事情在 JDK 5 以後 裝箱與拆箱操作 都由編譯器在編譯階段完成,即 程式碼片段1 都會在編譯階段被轉換爲 程式碼片段2

總結:

  1. 自動拆裝箱轉換的事情在 JDK 5 以後 裝箱與拆箱操作 都由編譯器在編譯階段完成

1.3 語法糖 - 泛型擦除

泛型也是在 JDK 5 開始加入的特性,但 java 在編譯泛型程式碼後會執行 泛型擦除 的動作,即泛型資訊在編譯爲位元組碼之後果就丟失了,實際的型別都當做了 Object 型別來處理: 

// 編譯期處理(語法糖)——泛型擦除
public class T03_CompileTime_GenericErase {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10); // 實際呼叫的是 List.add(Object e)
        Integer x = list.get(0); // 實際呼叫的是 Object obj = List.get(int index);
    }
}

所以在取值時,編譯器真正生成的位元組碼中,還要額外做一個型別轉換的操作:

// 需要將 Object 轉爲 Integer
Integer x = (Integer)list.get(0);

如果前面的 x 變數型別修改爲 int 基本型別那麼最終生成的位元組碼是:

// 需要將 Object 轉爲 Integer,並執行拆箱操作
int x = ((Integer)list.get(0)).intValue();

總結:

  1. 在JDK 5 及之後,泛型的型別轉換麻煩事都不用自己做,java編譯時就會幫我們做好。也就是說泛型參數只用於編譯階段的型別檢查,不影響執行階段
  2. 擦除的是位元組碼上的泛型資訊,可以看到 LocalVariableTypeTable 仍然保留了方法參數泛型的資訊

上述程式碼通過:javap -v T03_CompileTime_GenericErase.class進行反編譯,得到如下位元組碼。

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return
      LineNumberTable: ...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
            8      24     1  list   Ljava/util/List;
           31       1     2     x   Ljava/lang/Integer;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list   Ljava/util/List<Ljava/lang/Integer;>;

1.4.1 語法糖 - 泛型反射

使用反射,仍然能夠獲得這些資訊:

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
Method test = T03_CompileTime_GenericErase.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
    if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType)type;
        System.out.println("原始型別 - " + parameteredType.getRawType());
        Type[] arguments = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < arguments.length; i++) {
            System.out.printf("泛型參數[%d] - %s\n", i, arguments[i]);
        }
    }
}

輸出

原始型別 - interface java.util.List
泛型參數[0] - class java.lang.String
原始型別 - interface java.util.Map
泛型參數[0] - class java.lang.Integer
泛型參數[1] - class java.lang.Object

1.4 語法糖 - 可變參數

可變參數也是 JDK 5 開始加入的新特性,範例程式碼如下:

// 編譯期處理(語法糖)——可變參數
public class T04_CompileTime_VariableParameter {
    public static void foo(String... args) {
        String[] array = args; // 直接賦值
        System.out.println(array);
    }

    public static void main(String[] args) {
        foo("hello", "world");
    }
}

可變參數 String... args 其實是一個 String[] args,編譯器會將可變參數轉換爲陣列。從程式碼中賦值語句中就可以看出來。同樣 java 編譯器會在編譯期間將上述程式碼變換爲:

// 編譯期處理(語法糖)——可變參數
public class T04_CompileTime_VariableParameter {
    public static void foo(String[] args) { // 編譯器將可變參數轉換爲陣列
        String[] array = args; // 直接賦值
        System.out.println(array);
    }

    public static void main(String[] args) {
        foo("hello", "world");
    }
}

總結:

  1. 可變參數也是 JDK 5 開始加入的新特性;
  2. 可變參數 String... args 其實是一個 String[] args,編譯器會將可變參數轉換爲陣列;
  3. 如果呼叫了 foo() 則等價程式碼爲 foo(new String[] {}),建立了一個空的陣列,而不會傳遞 null 進去。

1.5 語法糖 - foreach 回圈

仍是 JDK 5 開始引入的語法糖,陣列的回圈:

// 編譯期處理(語法糖)——foreach
public class T05_CompileTime_Foreach {
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5}; // 陣列賦初值的簡化寫法也是語法糖哦
        for (int e : array) {
            System.out.println(e);
        }
    }
}

foreach 回圈會被編譯器轉換爲 fori 回圈:

// 編譯期處理(語法糖)——foreach
public class T05_CompileTime_Foreach {
    public T05_CompileTime_Foreach() {
    }

    public static void main(String[] args) {
        int[] array = new int[]{1,2,3,4,5}; 
        for (int i = 0; i < array.length; i++) {
            int e = array[i];
            System.out.println(e);
        }
    }
}

而集合的回圈:

// 編譯期處理(語法糖)——foreach
public class T05_CompileTime_Foreach {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

實際被編譯器轉換爲對迭代器的呼叫:

// 編譯期處理(語法糖)——foreach
public class T05_CompileTime_Foreach {
    public T05_CompileTime_Foreach() {
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        Iterator iter = list.iterator(); // 獲取迭代器
        while (iter.hasNext()) {
            Integer e = (Integer)iter.next(); // 型別轉換
            System.out.println(e);
        }
    }
}

總結:

  1. JDK 5 開始引入的語法糖
  2. 陣列的回圈 foreach 回圈會被編譯器轉換爲 fori 回圈
  3. 集合的回圈 實際被編譯器轉換爲對迭代器的呼叫
  4. foreach 回圈寫法,能夠配合數組,以及所有實現了 Iterable 介面的集合類一起使用,其中 Iterable 用來獲取集合的迭代器 (Iterator)

1.6 語法糖 - switch-string字串

從JDK 7 開始,switch 可以作用於字串和列舉類,這個功能其實也是語法糖,例如:

// 編譯期處理(語法糖)——switch-string
public class T06_CompileTime_SwitchString {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

注意:

  1. switch 配合 String 和列舉使用時,變數不能爲 null ,原因分析完語法糖轉換後的程式碼應當自然清楚

會被編譯器轉換爲:

// 編譯期處理(語法糖)——switch-string
public class T06_CompileTime_SwitchString {
    public T06_CompileTime_SwitchString() {

    }

    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()) {
            case 99162322: // hello 的 hashCode
                if (str.equals("hello")) {
                    x = 0;
                }
            case 113318802: // word 的 hashCode
                if (str.equals("world")) {
                    x = 1;
                }
        }
        switch (x) {
            case 0:
                System.out.println("h");
                break;
            case 1:
                System.out.println("w");
                break;
        }
    }
}

可以看到,執行了兩遍 switch,第一遍是根據字串的 hashCode equals 將字串的轉換爲相應的 byte 型別,第二遍才利用 byte 執行進行比較。

 

爲什麼第一遍時必須比較 hashCode ,又利用 equals 比較呢?hashCode 是爲了提高效率,減少可能的比較;而 equals 是爲了防止 hashCode 衝突,例如 BM 和 C. 這兩個字串的 hashCode 值都是 2123,如下程式碼:

// 編譯期處理(語法糖)——switch-string
public class T06_CompileTime_SwitchString2 {
    public static void choose(String str) {
        switch (str) {
            case "BM": {
                System.out.println("h");
                break;
            }
            case "C.": {
                System.out.println("w");
                break;
            }
        }
    }

會被編譯器轉換爲:

// 編譯期處理(語法糖)——switch-string
public class T06_CompileTime_SwitchString2 {
    public T06_CompileTime_SwitchString2() {

    }

    public static void choose(String str) {
        byte x = -1;
        switch (str.hashCode()) {
            case 2123: // hashCode 值可能相同,需要進一步用 equals 比較
                if (str.equals("C.")) {
                    x = 0;
                } else if (str.equals("BM")) {
                    x = 1;
                }
            default:
                switch (x) {
                    case 0:
                        System.out.println("h");
                        break;
                    case 1:
                        System.out.println("w");
                        break;
                }
        }
    }
}

總結:

  1. switch-string字串語法糖,從JDK 7 開始
  2. switch-string字串,會java 編譯器調整爲執行了兩遍 switch,第一遍是根據字串的 hashCode 和 equals 將字串的轉換爲相應的 byte 型別,第二遍才利用 byte 執行進行比較。 
  3. hashCode 是爲了提高效率,減少可能的比較;而 equals 是爲了防止 hashCode 衝突

1.7 語法糖 - switch 列舉

switch 列舉的例子,原始程式碼:

    enum Sex {
        MALE, FEMALE;
    }
// 編譯期處理(語法糖)——switch-enum
enum Sex {
    MALE, FEMALE;
}

public class T07_CompileTime_SwitchEnum {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男"); break;
            case FEMALE:
                System.out.println("女"); break;
        }
    }
}

轉換後程式碼:

public class T07_CompileTime_SwitchEnum {
    /**
     * 定義一個合成類(僅 jvm 使用,對我們不可見)
     * 用來對映列舉的 ordinal 與陣列元素的關係
     * 列舉的 ordinal 表示列舉物件的序號,從 0 開始
     * 即 MALE 的 ordinal()=0, FEMALE 的 ordinal()=1
     */
    static class $MAP {
        // 陣列大小即爲列舉元素個數,裏面儲存case用來對比的數位
        static int[] map = new int[2];
        static {
            map[Sex.MALE.ordinal()] = 1;
            map[Sex.FEMALE.ordinal()] = 2;
        }
    }

    public static void foo(Sex sex){
        int x = $MAP.map[sex.ordinal()];
        switch (x) {
            case 1:{
                System.out.println("男");
                break;
            }
            case 2:{
                System.out.println("女");
                break;
            }
        }
    }
}

總結:

  1. 列舉類使用 switch 時,java編譯器會幫我們自動合成一個靜態類用來對映列舉的 ordinal 下標 與陣列元素的關係,陣列大小即爲列舉元素個數,裏面儲存 case 是用來對比的數位;
  2. 接着,在 switch 方法中,取出列舉下標對應靜態類中定義的數位,進行switch判斷;

1.8 語法糖 - 列舉類

JDK 5 新增列舉類,以前面的性別的列舉爲例:

enum Sex {
    MALE, FEMALE;
}

java 編譯器將上述轉換後,得到如下程式碼:

public final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;

    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }
    /**
     * Sole constructor. Programmers cannot invoke this constructor.
     * It is for use code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position in the enum declaration, where the initial constant is assigned
     */
    private Sex(String name, int ordinal) {
        super(name, ordinal);
    }

    public static Sex[] values() {
        return $VALUES.clone();
    }

    public static Sex valueOf(String name) {
        return Enum.valueOf(Sex.class, name);
    }
}

總結:

  1. 列舉本質上是通過普通的類來實現的,只是編譯器爲我們進行了處理。每個列舉型別都繼承自 java.lang.Enum,並自動新增了 values 和 valueOf 方法。而每個列舉常數是一個靜態常數欄位,使用內部類實現,該內部類繼承了列舉類。所有列舉常數都通過靜態程式碼塊來進行初始化,即在類載入期間就初始化。另外通過把 clone、readObject、writeObject 這三個方法定義爲 final 的,同時實現是拋出相應的異常。這樣保證了每個列舉型別及列舉常數都是不可變的。可以利用列舉的這兩個特性來實現執行緒安全的單例。

1.9 語法糖 - try-with-resources

JDK 7 開始新增了對需要關閉的資源處理的特殊語法 try-with-resources

try (資源變數 = 建立資源物件) {

} catch {

}

其中資源物件需要實現 AutoCloseable 介面,例如:InputStreamOutputStreamConnectionStatementResultSet 等介面實現了 AutoCloseable,使用 try-with-resources 可以不用寫 finally 語句塊,編譯器會幫助生成 finally 程式碼關閉資源,例如:

// 編譯期處理(語法糖) —— try-with-resources
public class T09_CompileTime_TryWithResources {
    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("in/1.txt")){
            System.out.println(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

java 編譯器在編譯時,會做如下改動:

// 編譯期處理(語法糖) —— try-with-resources
public class T09_CompileTime_TryWithResources2 {
    public T09_CompileTime_TryWithResources2() {

    }
    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("in/1.txt");
            Throwable t  = null;
            try {
                System.out.println(is);
            } catch (Throwable e1) {
                // t 是我們程式碼出現的異常
                t = e1;
                throw e1;
            } finally {
                // 判斷了資源不爲空
                if (is != null) {
                    // 如果我們程式碼有異常
                    if (t != null) {
                        try {
                            is.close();
                        } catch (Throwable e2) {
                            // 如果 close 出現異常,作爲被壓制 壓製異常新增; 這樣異常不會丟
                            t.addSuppressed(e2); // 一般開發人員不會考慮這麼全面的異常捕獲
                        }
                    } else {
                        // 如果我們程式碼沒有異常,close 出現的異常就是最後的 catch 塊中的 e
                        is.close();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 什麼要設計一個 addSuppressed (Throwable e) (新增被壓制 壓製異常)的方法呢?是爲了防止異常資訊的丟失(想想 try-with-resources 生成的 finally 中如果拋出了異常):

// 編譯期處理(語法糖) —— try-with-resources
public class T09_CompileTime_TryWithResources3 {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource()) {
            int i = 1/0;
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    static class MyResource implements AutoCloseable {
        //@Override
        public void close() throws Exception {
            throw new Exception("close 異常");
        }
    }
}

如以上程式碼所示,兩個異常資訊都不會丟。 

總結:

  1. JDK 7 開始新增了對需要關閉的資源處理的特殊語法 try-with-resources
  2. 資源物件需要實現 AutoCloseable 介面,例如:InputStream、OutputStream、Connection、Statement、ResultSet 等介面實現了 AutoCloseable,使用 try-with-resources 可以不用寫 finally 語句塊,編譯器會幫助生成 finally 程式碼關閉資源
  3. try-with-resources 能實現正常資源的關閉,如果出現了異常,異常資訊不會丟失

1.10 語法糖 - 重寫橋接

我們都知道,方法重寫對對返回值分兩種情況:

  • 父子類的返回值完全一致
  • 子類返回值可以是父類別返回值的子類(比較繞口,見下面 下麪的例子)
class A {
    public Number m() {
        return 1;
    }
}

class B extends A {
    @Override
    // 子類 m 方法的返回值是 Integer 是父類別 m 方法返回值 Number 的子類
    public Number m() {
        return 2;
    }
}

對於子類,java編譯器會做如下處理:

class B extends A {
    public Integer m() {
        return 2;
    }
    // 此方法纔是真正重寫了父類別 public Number m() 方法; synthetic bridge 是jvm合成的,對我們不可見
    public synthetic bridge Number m() {
        // 呼叫 public Integer m()
        return m();
    }
}

其中橋接方法比較特殊,僅對 java 虛擬機器可見,並且與原來的 public Integer m() 沒有命名衝突,可以用下面 下麪反射程式碼來驗證:

for (Method m : B.class.getDeclaredMethods()) {
    System.out.println(m);
}

會輸出:

public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

總結:

  1. 父類別 A 的 m 的返回值是 Number 型別,子類 B 重寫 m 返回的值是 Integer 型別,對於 Java 語言是重寫的,但對於 Java 虛擬機器解析來說卻不是重寫的,只有當兩個方法的參數型別以及返回型別一致時,Java 虛擬機器纔會判定爲重寫,爲了保持重寫的語意,Java 編譯器會在 B類 的位元組碼檔案中自動生成一個橋接方法來保證重寫語意。

1.11 語法糖 - 匿名內部類

先來看一下匿名內部類的範例:

public class T11_Compile_HideInnerClass {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}

轉換後程式碼:java 編譯器會自動生成一個叫 xxx$1 的類,該類實現 Runnable 介面

final class T11_Compile_HideInnerClass$1 implements Runnable {
    T11_Compile_HideInnerClass$1() {
    }

    public void run() {
        System.out.println("ok")
    }
}

 然後在 xxx 類中,呼叫由 java 編譯器自動生成的 xxx$1 類物件

final class T11_Compile_HideInnerClass {
    public static void main(String[] args) {
        Runnable runnable = new T11_Compile_HideInnerClass$1();
    }
}

接下來,我們繼續看另外一個例子:參照區域性變數的匿名內部類,原始碼:

// 編譯期處理(語法糖) —— 匿名內部類
public class T11_Compile_HideInnerClass {
    public static void test(final int x) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
            }
        };
    }
}

java 編譯器會自動生成一個叫 xxx$1的類,該類實現 Runnable 介面。如有存在參照區域性變數,則會將區域性變數 x 做爲動態生成類 xxx$1 類的一個成員變數,通過構造方法傳入。 轉換後程式碼:

final class T11_Compile_HideInnerClass$1 implements Runnable {
    int val$x;
    T11_Compile_HideInnerClass$1(int x) {
        this.val$x = x;
    }

    public void run() {
        System.out.println("ok" + this.val$x)
    }
}
final class T11_Compile_HideInnerClass {
    public static void test(final int x) {
        Runnable runnable = new T11_Compile_HideInnerClass$1(x);
    }
}

注意:

這同時解釋了爲什麼匿名內部類參照區域性變數時,區域性變數必須是 final 的:因爲在建立 xxx$1 物件時,將 x 的值賦值給了 xxx$1 物件的 val$x 屬性,所以 x 不應該再發生變化了,如果變化,那 val$x 屬性沒有機會再跟着一起變化

 

總結:

  1. 匿名內部類的底層實現,由Java 編譯器自動生成一個叫 xxx$1的類,該類實現 Runnable 介面。如果存在參照區域性變數,則會將區域性變數 x 做爲動態生成類 xxx$1 類的一個成員變數,通過構造方法傳入。
  2. 區域性變數必須是 final 的:因爲在建立 xxx$1 物件時,將 x 的值賦值給了 xxx$1 物件的 val$x 屬性,所以 x 不應該再發生變化了,如果變化,那 val$x 屬性沒有機會再跟着一起變化

 


文章最後,給大家推薦一些受歡迎的技術部落格鏈接

  1. Hadoop相關技術部落格鏈接
  2. Spark 核心技術鏈接
  3. JAVA相關的深度技術部落格鏈接
  4. 超全乾貨--Flink思維導圖,花了3周左右編寫、校對
  5. 深入JAVA 的JVM核心原理解決線上各種故障【附案例】
  6. 請談談你對volatile的理解?--最近小李子與面試官的一場「硬核較量」
  7. 聊聊RPC通訊,經常被問到的一道面試題。原始碼+筆記,包懂

 


歡迎掃描下方的二維條碼或 搜尋 公衆號「10點進修」,我們會有更多、且及時的資料推播給您,歡迎多多交流!