Java 中基於值的類
一、簡介
在本教程中,我們將討論 Valhalla 專案為 Java 生態系統帶來的一個非常有趣的功能,即基於值的類別。基於值的類別在 Java 8 中引入,並在後續版本中進行了重大重構和增強。
2. 基於價值的課程
2.1.瓦爾哈拉計劃
Valhalla 專案是 OpenJDK 的實驗項目,旨在為 Java 添加新功能和功能。該計劃的主要目標是增加對值類型、通用專業化和性能改進的改進支持,同時保持完全的向後相容性。
基於值的類別是 Valhalla 專案引入的功能之一,旨在為 Java 語言引入原始的、不可變的值,而不會增加傳統物件導向類別帶來的額外開銷。
2.2.原語和值類型
在我們正式定義基於值的類別之前,讓我們先來看看 Java 中的兩個重要語意——基元和值類型。
Java 中的原始資料類型或原語是表示單一值的簡單資料類型,而不是物件。 Java 提供了八種這樣的基本資料型別: byte
、 short
、 int
、 long
、 float
、 double
、 char,
和boolean
。雖然這些都是簡單類型,但 Java 為每個類型提供了包裝類,以便我們以物件導向的方式與它們進行互動。
同樣重要的是要記住,Java 會自動執行自動裝箱和拆箱,以有效地在物件類型和基本類型之間進行轉換:
int primitive_a = 125;
Integer obj_a = 125; // this is autoboxed
Assert.assertSame(primitive_a, obj_a);
原始類型位於堆疊記憶體中,而我們在程式碼中使用的物件則位於堆疊記憶體中。
Valhalla 專案在 Java 生態系統中引入了介於物件和基元之間的新類型,稱為值類型。值類型是不可變類型,且它們沒有任何標識。這些值類型也不支援繼承。
值類型不是透過其引用而是透過其值來尋址,就像基元一樣。
2.3.基於值的類別
基於值的類別是被設計為行為類似於 Java 中的值類型並封裝value-type
類別。 JVM 可以在值類型和基於值的類別之間自由切換,就像自動裝箱和拆箱一樣。因此,基於同樣的原因,基於價值的類別是與身分無關的。
3. 基於值的類別的屬性
基於值的類別是表示簡單不可變值的類別。基於值的類別具有多個屬性,可以將這些屬性分類為一些常規主題。
3.1.不變性
基於值的類別旨在表示不可變數據,類似於int,
並具有以下特徵:
- 基於價值的課程始終是
final
- 它僅包含
final
字段 - 該類別可以擴展
Object
類別或不聲明實例字段的抽象類別的層次結構
3.2.物件創建
讓我們了解創建基於值的類別的新物件是如何運作的:
- 該類別沒有聲明任何可訪問的建構函數
- 如果存在可存取的建構函數,則應將它們標記為已棄用以便刪除
- 該類別只能透過工廠方法實例化。從工廠接收的實例可能是也可能不是新實例,並且呼叫程式碼不應對其身分做出任何假設
3.3. Identity 和equals()
、 hashCode(),
toString()
方法
基於價值的類別是與身分無關的。由於它們仍然是 Java 中的類,因此我們需要了解從Object
類別繼承的方法是如何發生的:
-
equals()
、hashCode(),
和toString()
的實作僅根據其實例成員的值定義,而不是根據它們的身分或任何其他實例的狀態 - 我們認為兩個物件僅在物件的
equals()
檢查上相等,而不是基於引用的相等,即 == - 我們可以互換使用兩個相等的對象,並且它們應該在任何計算或方法呼叫上產生相同的結果。
3.4.一些額外的注意事項
在使用基於值的類別時,我們應該考慮一些額外的限制:
- 根據
equals()
方法相等的兩個對象可能是 JVM 中的不同對象,也可能是同一個對象 - 我們無法確保監視器的獨佔所有權,使得實例不適合同步
4. 基於值的類別的範例
4.1. JDK 中基於值的類
JDK 中有幾個類別遵循基於值的類別規範。
首次引入時, java.util.Optional
和 DateTime API ( java.time.LocalDateTime
) 是基於價值的課程。從 Java 16 及更高版本開始,Java 已將Integer
和Long
等基本類型的所有包裝類別定義為基於值的。
這些類別具有jdk.internal
包中的@ValueBased
註解:
@jdk.internal.ValueBased
public final class Integer extends Number implements Comparable<Integer>, Constable, ConstantDesc {
// Integer class in the JDK
}
4.2.自訂基於值的類別
讓我們建立遵循上面定義的基於值的類別規範的自訂類別。對於我們的範例,我們採用一個Point
類別來標識 3D 空間中的一個點。此類別有 3 個整數欄位x
、 y,
和z
。
我們可以認為, Point
定義是基於值的類別的良好候選者,因為空間中的特定點是唯一的,只能透過其值來引用。它是恆定且明確的,很像整數 302。
我們首先將類別定義為final
,並將其屬性x
、 y,
和z
定義為final。我們也將建構函式設為私有:
public final class Point {
private final int x;
private final int y;
private final int z;
// inaccessible constructor
private Point(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
// ...
}
現在,讓我們預先建立該類別的origin(0, 0, 0)
實例,每次呼叫建立x = 0
、 y = 0,
和z = 0:
private static Point ORIGIN = new Point(0, 0, 0);
我們現在需要以工廠方法的形式提供物件創建機制:
public static Point valueOfPoint(int x, int y, int z) {
// returns a cached instance if it is the origin, or a new instance
if (isOrigin(x, y, z)) {
return ORIGIN;
}
return new Point(x, y, z);
}
// checking if a point is the origin
private static boolean isOrigin(int x, int y, int z) {
return x == 0 && y == 0 && z == 0;
}
工廠方法valueOfPoint()
可以根據參數傳回一個新實例或快取實例。這迫使呼叫程式碼不對物件的狀態做出任何假設或比較兩個實例的參考。
最後,我們應該只根據實例欄位的值定義equals()
方法:
@Override
public boolean equals(Object other) {
if (other == null || getClass() != other.getClass()) {
return false;
}
Point point = (Point) other;
return x == point.x && y == point.y && z == point.z;
}
@Override
public int hashCode() {
return Objects.hash(x, y, z);
}
我們現在有一個Point
類,它可以充當基於值的類別。從jdk.internal
套件匯入後,我們可以將@ValueBased
註解加入到該類別中。然而,這對我們的案例來說並不是強制性的。
現在讓我們測試 (1,2,3) 表示的空間中同一點的兩個實例是否相等:
@Test
public void givenValueBasedPoint_whenCompared_thenReturnEquals() {
Point p1 = Point.valueOfPoint(1,2,3);
Point p2 = Point.valueOfPoint(1,2,3);
Assert.assertEquals(p1, p2);
}
此外,為了進行本練習,我們也可以看到,如果透過引用進行比較,則在建立兩個origin
時,兩個實例是相同的:
@Test
public void givenValueBasedPoint_whenOrigin_thenReturnCachedInstance() {
Point p1 = Point.valueOfPoint(0, 0, 0);
Point p2 = Point.valueOfPoint(0, 0, 0);
// the following should not be assumed for value-based classes
Assert.assertTrue(p1 == p2);
}
5. 基於值的類別的優點
現在我們知道什麼是基於值的類別以及如何定義基於值的類,讓我們了解為什麼我們可能需要基於值的類別。
基於值的類別是 Valhalla 規範的一部分,仍處於實驗階段並繼續發展。因此,此類課程的好處可能會隨著時間的推移而改變。
到目前為止,使用基於值的類別帶來的最重要的好處是記憶體利用率。基於值的類別具有更高的記憶體效率,因為它們沒有基於引用的標識。此外,JVM 可以重複使用現有實例或根據需求建立新實例,從而減少記憶體佔用。
此外,它們不需要同步,從而提高了整體效能,尤其是在多執行緒應用程式中。
6. 基於值的類別與其他類型的區別
6.1.不可變類
Java 中的不可變類別與基於值的類別有許多共同點。因此,了解它們之間的差異非常重要。
雖然基於值的類別是新的並且是正在進行的實驗功能的一部分,但不可變類別長期以來一直是 Java 生態系統的核心和組成部分。 Java 中的String
類別、 Enums,
和包裝類別(例如Integer
類別)都是不可變類別的範例。
不可變類別不像基於值的類別那樣是無身分的。具有相同狀態的 Immutable 類別的實例是不同的,我們可以根據引用相等性來比較它們。基於值的類別的實例沒有基於引用的相等概念:
不可變類別可以自由地提供可存取的建構函數,並且可以具有多個屬性和複雜的行為。然而,基於值的類別表示簡單的值,並且不定義具有依賴屬性的複雜行為。
最後,我們應該注意到,根據定義,基於值的類別是不可變的,但反之則不然。
6.2.記錄
Java 在 Java 14 中引入了Records
的概念,作為傳遞不可變資料物件的簡單方法。記錄和基於值的類別實現不同的目的,即使它們在行為和語義上看起來相似。
記錄和基於值的類別之間最明顯的差異是記錄具有公共建構函數,而基於值的類別則缺少公共建構函數。
七、結論
在本文中,我們討論了 Java 中基於值的類別和值類型的概念。我們談到了基於價值的階級必須遵守的重要屬性及其帶來的好處。我們也討論了基於值的類別和類似的 Java 概念(例如不可變類別和記錄)之間的差異。
與往常一樣,本文中使用的程式碼片段可以在 GitHub 上找到。