在上一篇文章實戰 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;
}
}
如果沒有自己指定,則會自動生成這樣一個方法:
- 方法名就是屬性名稱
- 返回類型就是對應的屬性類型
- 是一個 public 方法,並且沒有聲明拋出任何異常
- 方法體就是返回對應屬性
- 如果屬性上面有任何注解,那么這個注解如果能加到方法上那么也會自動加到這個方法上。例如:
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()
這三個方法。這三個方法的實現方式,在第一節已經詳細分析過,這里簡單回顧下要點:
hashcode()
在編譯的時候自動生成字節碼實現,核心邏輯基於ObjectMethods
的makeHashCode
方法,里面的邏輯是對於每一個屬性的哈希值移位組合起來。注意這里的所有調用(包括對於ObjectMethods
的方法調用以及獲取每個屬性)都是利用MethodHandle
實現的近似於直接調用的方式調用的。equals()
在編譯的時候自動生成字節碼實現,核心邏輯基於ObjectMethods
的makeEquals
方法,里面的邏輯是對於兩個 Record 對象每一個屬性判斷是否相等(對於引用類型用Objects.equals()
,原始類型使用 ==),注意這里的所有調用(包括對於ObjectMethods
的方法調用以及獲取每個屬性)都是利用MethodHandle
實現的近似於直接調用的方式調用的。toString()
在編譯的時候自動生成字節碼實現,核心邏輯基於ObjectMethods
的makeToString
方法,里面的邏輯是對於每一個屬性的值組合起來構成字符串。注意這里的所有調用(包括對於ObjectMethods
的方法調用以及獲取每個屬性)都是利用MethodHandle
實現的近似於直接調用的方式調用的。
Record 構造器
如果沒有指定構造器,Record 類會自動生成一個以所有屬性為參數並且給每個屬性賦值的構造器。我們可以通過兩種方式自己聲明構造器:
第一種是明確聲明以所有屬性為參數並且給每個屬性賦值的構造器,這個構造器需要滿足:
- 構造器參數需要包括所有屬性(同名同類型),並按照 Record 類頭的聲明順序。
- 不能聲明拋出任何異常(不能使用 throws)
- 不能調用其他構造器(即不能使用 this(xxx))
- 構造器需要對於每個屬性進行賦值。
對於其他構造器,需要明確調用這個包含所有屬性的構造器:
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: