Java 25 中的穩定值
1.概述
在本教程中,我們將探索 Java 25 中引入的預覽穩定值 API(JEP 502)。
2. 理解問題
作為 Java 開發人員,我們不斷權衡與使我們的字段final
相關的利弊!
一方面,為了使我們的類別不可變, final
實例欄位是必要的,這有助於提升程式碼的簡潔性和健全性。此外,JVM 編譯器javac
和JIT
會在進行常數折疊最佳化時考慮某些final
字段,這些最佳化會透過提前簡化/計算常數表達式來消除編譯程式碼中不必要的計算。
然而,使用final
字段有一個很大的限制。我們必須立即初始化final
字段。如果字段的創建成本很高,那麼就會出現問題。
假設我們有一個帶有昂貴物件的不可變類別:
class ImmutableClass {
private final ExpensiveObject expensiveObject = new ExpensiveObject();
public ImmutableClass() {}
void methodThatUsesInstanceField() {
// logic that uses instance field
}
}
我們可以看到,每次實例化我們的類別時,都會產生初始化expensiveObject
欄位的開銷。如果這是一個static
字段,那麼我們會在類別載入時產生這個開銷:
static final ExpensiveObject expensiveObject = new ExpensiveObject();
提前初始化會導致 Java 應用程式啟動時間過長。理想情況下,我們希望將這些初始化開銷推遲到真正需要使用這些欄位的時候。我們可以刪除final
修飾符,在第一次讀取時設定字段。但是,這種方法不是線程安全的。
相反,穩定值 API 支援不變性,同時也延遲初始化,或者換句話說,「延遲不變性」。
3. 什麼是StableValue?
StableValue
充當字段的包裝器,我們將其稱為其內容。此欄位帶有@Stable
(JDK 內部註解),保證其在生命週期內最多初始化一次。
重要的是,這在多執行緒並發環境下得到了保證。由於此保證,只要StableValue
欄位是final
,JVM 就可以像對待記錄中的可信任final
欄位一樣進行常數折疊最佳化。除了確保初始化後內容始終指向同一個物件之外,我們還可以使用 Stable Values API 延遲初始化。
3.1. 使用StableValue.of()
方法建立StableValue
根據StableValue
內容是否已初始化,可以將其視為未設定或已設定。
假設我們想要使用靜態工廠方法StableValue.of()
來建立一個未設定的StableValue
:
StableValue<ExpensiveObject> expensiveObject = StableValue.of();
此時,在expensiveObject上呼叫isSet()
將會回傳false
。
3.2. 設定StableValue
現在我們有一個未設定的StableValue
,我們可以使用一系列方法來設定它的內容。其中一個方法是orElseSet(T contents).
如果內容目前未設置,則此方法將設置內容;否則,它什麼也不做。將其應用於我們的ImmutableClass
,我們得到:
class ImmutableClass {
private final StableValue<ExpensiveObject> expensiveObject = StableValue.of();
void methodThatUsesInstanceField() {
expensiveObject.orElseSet(new ExpensiveObject());
// logic that uses instance field
}
}
正如我們所見,我們已經實現了類別的不變性以及延遲初始化。
我們也可以使用其他方法來設定StableValue
。如果在已設定的StableValue
上呼叫setOrThrow(T contents)
方法,則會拋出例外狀況。而如果內容目前未設定且已初始化為提供的參數,則trySet(T contents)
方法將會傳回true
。否則,如果內容已設置, false is
。
3.3. 使用StableValue.of(T contents)
方法建立一組StableValue
或者,我們可以直接使用靜態工廠方法StableValue.of(T contents)
來建立一個集合StableValue
:
StableValue<ExpensiveObject> expensiveObject = StableValue.of(new ExpensiveObject());
但是,如果我們將其用於實例字段,就不再需要延遲初始化了。為了指定如何在聲明時初始化內容並保持延遲初始化,我們可以改用StableSupplier
。
4. StableSupplier
Stable Value API 提供了靜態工廠方法StableValue.supplier(Supplier<? extends T> underlying),
該方法傳回一個StableSupplier
.
StableSupplier
實作了Supplier<T>
接口,並包含兩個欄位:一個StableValue
物件和一個提供的 supplier 參數。
建立StableSupplier
後,我們可以呼叫重寫的get()
方法。此方法將檢查StableValue
欄位。如果未設置,則使用底層供應商初始化其內容。否則,它將傳回已設定的StableValue
的內容。這具有與以前相同的並發保證。
我們現在可以在ImmutableClass
中的同一個語句中重新統一宣告和初始化:
class ImmutableClass {
private final Supplier<ExpensiveObject> expensiveObject = StableValue.supplier(() -> new ExpensiveObject());
void methodThatUsesInstanceField() {
// logic that uses instance field
}
}
請注意我們如何在宣告時指定內容的初始化,同時實作延遲初始化。
但是,如果我們有一個變數集合,並且想要延遲初始化,該怎麼辦呢?我們可以使用Stable Collections
來實現!
5. 穩定集合
5.1. StableList
穩定值 API 提供了靜態工廠方法StableValue.list(int size, IntFunction<? extends E> mapper),
該方法傳回一個StableList
物件。它是List<E>
的子類型。第一個參數是一個int
,用於確定此list
的大小。第二個參數是一個IntFunction
,它接受某個元素的索引來計算其預期值。
重要的是,此計算僅在我們首次訪問元素時發生。任何後續存取該元素的操作都會傳回來自StableValue
集合的快取結果。我們透過List
介面的典型方法(例如get(int index)
存取元素。
假設我們想要計算 5 的乘法表到 10 的倍數。我們可以這樣做:
List<Integer> fiveTimesTable = List.of(0 * 5, 1 * 5,..., 10 * 5);
然而,假設計算每個倍數的計算成本非常高。因此,我們希望僅在首次存取每個元素時才進行計算,並將其快取以備將來使用。我們可以使用StableList
來實現這一點:
@Test
void givenStableListForFiveTimesTable_thenVerifyElementsAreExpected() {
List<Integer> fiveTimesTable = StableValue.list(11, index -> index * 5);
assertThat(fiveTimesTable.get(0)).isEqualTo(0);
assertThat(fiveTimesTable.get(1)).isEqualTo(5);
// ...
assertThat(fiveTimesTable.get(10)).isEqualTo(50);
}
值得注意的是,如果我們有以下語句:
assertThat(fiveTimesTable.get(0)).isEqualTo(0);
assertThat(fiveTimesTable.get(0)).isEqualTo(0); // returns the already-computed value
我們只計算第一個語句的表達式0 * 5
。
5.2. StableMap
StableMap
本質與StableList
非常相似。不過,這次我們想延遲初始化一Set
鍵值對的昂貴值。這些鍵值被提供給靜態工廠方法StableValue.map(Set<K> keys, Function<? super K, ? extends V> mapper)
以及一個用於計算特定鍵值的 mapper 函數。
當我們第一次存取某個鍵對應的值StableValue
,該函數就會被呼叫。 StableValue 用於快取該結果並傳回結果。
假設我們有一Set
城市和一個昂貴的函數來確定特定城市位於哪個國家:
Set<String> cities = Set.of("London", "Madrid", "Paris");
我們可以使用StableMap
來確保我們只承擔計算我們所關注的國家的成本:
@Test
void givenStableMapForCityToCountry_thenVerifyValuesAreExpected() {
Map<String, String> cityToCountry = StableValue.map(
cities, city -> expensiveMethodToDetermineCountry(city)
);
assertThat(cityToCountry.get("London")).isEqualTo("England");
assertThat(cityToCountry.get("Madrid")).isEqualTo("Spain");
assertThat(cityToCountry.get("Paris")).isEqualTo("France");
}
此外,我們還確保每個值的計算僅在第一次訪問時發生。
6. StableFunction
穩定值 API 也提供了靜態工廠方法StableValue.function(Set<? extends T> inputs, Function<? super T, ? extends R> underlying),
該方法傳回一個StableFunction
或StableEnumFunction
。重要的是,這兩個類別都實作了Function
介面。第一個參數定義了一組預期輸入。我們只能在呼叫apply()
方法時使用這些輸入。如果我們使用不在此集合中的值進行調用,則會引發異常。
重寫的apply()
方法在首次呼叫時,會使用提供的函數為特定輸入設定StableValue
。因此,後續對該輸入的任何apply()
呼叫都會傳回已設定的StableValue
的內容。
修改我們之前的城市範例,我們可以使用StableFunction
:
@Test
void givenStableFunctionForCityToCountry_whenValidInputsUsed_thenVerifyFunctionResultsAreExpected() {
Function<String, String> cityToCountry = StableValue.function(
cities, city -> expensiveMethodToDetermineCountry(city)
);
assertThat(cityToCountry.apply("London")).isEqualTo("England");
assertThat(cityToCountry.apply("Madrid")).isEqualTo("Spain");
assertThat(cityToCountry.apply("Paris")).isEqualTo("France");
}
當我們使用無效輸入時,我們會收到異常:
@Test
void givenStableFunctionForCityToCountry_whenInvalidInputUsed_thenExceptionThrown() {
Set<String> cities = Set.of("London", "Madrid", "Paris"); // doesn't include "Berlin"
Function<String, String> cityToCountry = StableValue.function(
cities, city -> expensiveMethodToDetermineCountry(city)
);
assertThatIllegalArgumentException().isThrownBy(() -> cityToCountry.apply("Berlin"));
}
最後,還有一個靜態工廠方法intFunction(int size, IntFunction<? extends R> underlying)
,它回傳一個StableIntFunction
。我們可以在處理ints/Integers
時使用此方法,因為size
參數定義了有效int
輸入的範圍。
7. 結論
在本文中,我們探討了穩定值 API 的用途,並深入討論如何使用基於穩定值建構的高階物件來處理不同的用例。此外,由於這些高階物件的內部使用了穩定值,因此可以進行常數折疊最佳化。
本文中的所有程式碼片段都可以在 GitHub 上瀏覽。