實戰 Java 16 值型別 Record - 2. Record 的基本用法

乾貨滿滿張雜湊發表於2021-04-16

在上一篇文章實戰 Java 16 值型別 Record - 1. Record 的預設方法使用以及基於預編譯生成相關位元組碼的底層實現中,我們詳細分析了 Record 自帶的屬性以及方法和底層位元組碼與實現。這一篇我們來詳細說明 Record 類的用法。

宣告一個 Record

Record 可以單獨作為一個檔案的頂級類,即:
User.java 檔案:

public record User(long id, String name, int age) {}

也可以作為一個成員類,即:

public class RecordTest {
    public record User(long id, String name, int age) {}
}

也可以作為一個本地類,即:

public class RecordTest {
    public void test() {
        record Mail (long id, String content){}
        Mail mail = new Mail(10, "content");
    }
}

不能用 abstract 修飾 Record 類,會有編譯錯誤。
可以用 final 修飾 Record 類,但是這其實是沒有必要的,因為 Record 類本身就是 final 的

成員 Record 類,還有本地 Record 類,本身就是 static 的,也可以用 static 修飾,但是沒有必要。

和普通類一樣,Record 類可以被 public, protected, private 修飾,也可以不帶這些修飾,這樣就是 package-private 的。

和一般類不同的是,Record 類的直接父類不是 java.lang.Object 而是 java.lang.Record。但是,Record 類不能使用 extends,因為 Record 類不能繼承任何類。

Record 類的屬性

一般,在 Record 類宣告頭部指定這個 Record 類有哪些屬性

public record User(long id, String name, int age) {}

同時,可以在頭部的屬性列表中運用註解:

@Target({ ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
public @interface A {}
@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface B {}

public record User(@A @B long id, String name, int age) {}

但是,需要注意一點,這裡通過反射獲取 id 的註解的時候,需要通過對應的方式進行獲取,否則獲取不到,即 ElementType.FIELD 通過 Field 獲取,ElementType.RECORD_COMPONENT 通過 RecordComponent 獲取:

Field[] fields = User.class.getDeclaredFields();
Annotation[] annotations = fields[0].getAnnotations(); // 獲取到註解 @B

RecordComponent[] recordComponents = User.class.getRecordComponents();
annotations = recordComponents[0].getAnnotations(); // 獲取到註解 @A

Record 類體

Record 類屬性必須在頭部宣告,在 Record 類體只能宣告靜態屬性

public record User(long id, String name, int age) {
    static long anotherId;
}

Record 類體可以宣告成員方法和靜態方法,和一般類一樣。但是不能宣告 abstract 或者 native 方法

public record User(long id, String name, int age) {
    public void test(){}
    public static void test2(){}
}

Record 類體也不能包含例項初始化塊,例如:

public record User(@A @B long id, String name, int age) {
    {
        System.out.println(); //編譯異常
    }
}

Record 成員

Record 的所有成員屬性,都是 public final 非 static 的,對於每一個屬性,都有一個對應的無引數返回型別為屬性型別方法名稱為屬性名稱的方法,即這個屬性的 accessor。前面說這個方法是 getter 方法其實不太準確,因為方法名稱中並沒有 get 或者 is 而是隻是純屬性名稱作為方法名。

這個方法如果我們自己指定了,就不會自動生成:

public record User(long id) {
    @Override
    public long id() {
        return id;
    }
}

如果沒有自己指定,則會自動生成這樣一個方法:

  1. 方法名就是屬性名稱
  2. 返回型別就是對應的屬性型別
  3. 是一個 public 方法,並且沒有宣告丟擲任何異常
  4. 方法體就是返回對應屬性
  5. 如果屬性上面有任何註解,那麼這個註解如果能加到方法上那麼也會自動加到這個方法上。例如:
public record User(@A @B long id, String name, int age) {}
@Target({
        ElementType.RECORD_COMPONENT,
        ElementType.METHOD,
})
@Retention(RetentionPolicy.RUNTIME)
public @interface A {}
@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface B {}

下面獲取 id() 這個方法的註解則會獲取到註解 @A

Method id = User.class.getDeclaredMethod("id");
Annotation[] idAnnotations = id.getAnnotations(); //@A

由於會自動生成這些方法,所以 Record 成員的名稱不能和 Object 的某些不符合上述條件(即上面提到的 6 條)的方法的名稱一樣,例如:

public record User(
    int wait, //編譯錯誤
    int hashcode, //這個不會有錯誤,因為 hashcode() 方法符合自動生成的 accessor 的限制條件。
    int toString, //編譯錯誤
    int finalize //編譯錯誤) {
}

Record 類如果沒有指定,則預設會生成實現 java.lang.Record 的抽象方法,即hashcode(), equals(), toStrng() 這三個方法。這三個方法的實現方式,在第一節已經詳細分析過,這裡簡單回顧下要點:

  1. hashcode() 在編譯的時候自動生成位元組碼實現,核心邏輯基於 ObjectMethodsmakeHashCode 方法,裡面的邏輯是對於每一個屬性的雜湊值移位組合起來。注意這裡的所有呼叫(包括對於 ObjectMethods 的方法呼叫以及獲取每個屬性)都是利用 MethodHandle 實現的近似於直接呼叫的方式呼叫的。
  2. equals() 在編譯的時候自動生成位元組碼實現,核心邏輯基於 ObjectMethodsmakeEquals 方法,裡面的邏輯是對於兩個 Record 物件每一個屬性判斷是否相等(對於引用型別用Objects.equals(),原始型別使用 ==),注意這裡的所有呼叫(包括對於 ObjectMethods 的方法呼叫以及獲取每個屬性)都是利用 MethodHandle 實現的近似於直接呼叫的方式呼叫的。
  3. toString() 在編譯的時候自動生成位元組碼實現,核心邏輯基於 ObjectMethodsmakeToString 方法,裡面的邏輯是對於每一個屬性的值組合起來構成字串。注意這裡的所有呼叫(包括對於 ObjectMethods 的方法呼叫以及獲取每個屬性)都是利用 MethodHandle 實現的近似於直接呼叫的方式呼叫的。

Record 構造器

如果沒有指定構造器,Record 類會自動生成一個以所有屬性為引數並且給每個屬性賦值的構造器。我們可以通過兩種方式自己宣告構造器:

第一種是明確宣告以所有屬性為引數並且給每個屬性賦值的構造器,這個構造器需要滿足:

  1. 構造器引數需要包括所有屬性(同名同型別),並按照 Record 類頭的宣告順序。
  2. 不能宣告丟擲任何異常(不能使用 throws)
  3. 不能呼叫其他構造器(即不能使用 this(xxx))
  4. 構造器需要對於每個屬性進行賦值。

對於其他構造器,需要明確呼叫這個包含所有屬性的構造器

public record User(long id, String name, int age) {
    public User(int age, long id) {
        this(id, "name", age);
    }
    public User(long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

第二種是簡略方式宣告,例如:

public record User(long id, String name, int age) {
    public User {
        System.out.println("initialized");
        id = 1000 + id;
        name = "prefix_" + name;
        age = 1 + age;
        //在這之後,對每個屬性賦值
    }
}

這種方式相當於省略了引數以及對於每個屬性賦值,相當於對這種構造器的開頭插入程式碼。

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

image

相關文章