16 安定値
安定値は、そのコンテンツと呼ばれる単一のデータ値を保持するStableValue型のオブジェクトです。安定値のコンテンツは不変です。JVMは、安定値を定数として処理し、finalフィールドと同じパフォーマンスの最適化を可能にします。ただし、finalフィールドと比較して、安定値のコンテンツを初期化するときの柔軟性が増します。
ノート:
これはプレビュー機能です。プレビュー機能は、設計、仕様および実装が完了したが、永続的でない機能です。プレビュー機能は、将来のJava SEリリースで、異なる形式で存在することもあれば、まったく存在しないこともあります。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。『Preview Language and VM Features』を参照してください。安定値の背景情報は、JEP 502を参照してください。
ロガーをfinalフィールドとして宣言および初期化する次の例を考えてみます:
public class Locations {
private final Logger logger =
Logger.getLogger(Locations.class.getName());;
Logger getLogger() {
return logger;
}
public void printLocations() {
getLogger().info("Printing locations...");
}
}loggerはfinalフィールドであるため、Locationsのインスタンスの作成時に初期化する必要があります。これは、即時初期化の例です。ただし、例では、printLocations()がコールされるまでロガーを使用しません。このメソッドがコールされたときにloggerを初期化するには、遅延初期化を使用できます。これは、必要な場合にのみ結果が生成されることを意味します(この例では、loggerフィールドが初期化されます):
public class Customers {
private Logger logger = null;
synchronized Logger getLogger() {
if (logger == null) {
logger = Logger.getLogger(Customers.class.getName());
}
return logger;
}
public void printCustomers() {
getLogger().info("Printing customers...");
}
}この例では、loggerが初期化されるのは、getLogger()がコールされたときのみで、loggerが以前に初期化されていない場合のみです。ただし、この方法にはいくつかのデメリットがあります:
- コードは、
getLogger()メソッドを介してloggerフィールドにアクセスする必要があります。そうでない場合は、loggerが初期化されていない場合にNullPointerExceptionがスローされます。 - スレッド競合は、複数のスレッドが
loggerを同時に初期化しようとしたときに発生する可能性があります。この例では、getLogger()をsynchronizedとして宣言します。結果として、スレッドがこのメソッドを呼び出すと、他のすべてのスレッドがそのメソッドを呼び出さないようにブロックされます。これにより、ボトルネックが発生し、アプリケーションが遅くなる可能性があります。ただし、getLogger()をsynchronizedとして宣言しない場合、複数のロガー・オブジェクトが作成される可能性があります。 loggerはfinalとして宣言されていないため、コンパイラは定数に関連するパフォーマンス最適化を適用できません。
loggerが初期化されたときに、遅延初期化と不変の両方であることが理想的です。つまり、不変性を遅延することが理想的です。これを行うには、安定値を使用します:
public class Orders {
private final StableValue<Logger> logger = StableValue.of();
Logger getLogger() {
return logger.orElseSet(
() -> Logger.getLogger(Orders.class.getName()));
}
public void printOrders() {
getLogger().info("Printing orders...");
}
}静的ファクトリ・メソッドStableFactory.of()は、コンテンツを保持しない安定値を作成します:
private final StableValue<Logger> logger = StableValue.of();StableValue::orElseSet(Supplier)は、安定値loggerのコンテンツを取得します。ただし、安定値にコンテンツが含まれていない場合、orElseSetは指定されたSupplier:を使用してコンテンツを計算および設定しようとしますreturn logger.orElseSet(
() -> Logger.getLogger(Orders.class.getName()));Ordersの例では、安定値が使用されたとき、つまりgetLoggerが呼び出されたときに初期化されます。ただし、Customersの例と同様に、コードはgetLogger()メソッドを介してlogger安定値にアクセスする必要があります。
java.util.function.Supplierタイプの安定サプライヤを使用して、実際に初期化せずに宣言するときに安定値を初期化する方法を指定できます。その後、サプライヤのget()メソッドを呼び出すことで、安定値にアクセスできます。
public class Products {
private final Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(Products.class.getName()));
public void printProducts() {
logger.get().info("Printing products...");
}
}メソッドStableValue.supplier(Supplier)は、java.util.function.Supplierを返します:
private final Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(Products.class.getName()));この例では、logger.get()の最初の呼出しによって、StableValue.supplier(Supplier)の引数として指定されたラムダ式が呼び出されます:
() -> Logger.getLogger(Products.class.getName())StableValue.supplier(Supplier)メソッドは、安定値のコンテンツをこのラムダ式の結果の値で初期化し、値を返します。この例では、新しいロガー・インスタンスです。logger.get()のその後の呼出しでは、安定値のコンテンツがすぐに返されます。
安定値の集計および構成
1つのアプリケーションで複数の安定値を集計できるため、起動時間を短縮できます。また、他の安定値から安定値を構成できます。前述の例のOrdersおよびProductsは、ロガー・コンポーネントを安定値に格納する方法を示しています。次の例では、Orders、Products、LocationsおよびCustomersコンポーネントを独自の安定値に格納します。
public class Application {
static final StableValue<Locations> LOCATIONS = StableValue.of();
static final StableValue<Orders> ORDERS = StableValue.of();
static final StableValue<Customers> CUSTOMERS = StableValue.of();
static final StableValue<Products> PRODUCTS = StableValue.of();
public static Locations locations() {
return LOCATIONS.orElseSet(Locations::new);
}
public static Orders orders() {
return ORDERS.orElseSet(Orders::new);
}
public static Customers customers() {
return CUSTOMERS.orElseSet(Customers::new);
}
public static Products products() {
return PRODUCTS.orElseSet(Products::new);
}
public void main(String[] args) {
locations().printLocations();
orders().printOrders();
customers().printCustomers();
products().printProducts();
}
}
アプリケーションのコンポーネントを安定値に格納することで、アプリケーションの起動時間を大幅に改善できます。アプリケーションでは、すべてのコンポーネントが事前に初期化されなくなります。この例では、安定値のorElseSetメソッドを介して、オンデマンドでコンポーネントを初期化します。
OrdersおよびProductsコンポーネントは、ロガーに独自の内部安定値を使用していることに注意してください。特に、安定値ORDERSは、安定値Orders.loggerに依存します。ORDERSがまだ存在しない場合は、依存するOrders.loggerが最初に作成されます。PRODUCTSおよびProducts.loggerの安定値についても同様です。
循環依存関係がある場合は、次の例に示すIllegalStateExceptionがスローされます:
public class StableValueCircularDependency {
public static class A {
static final StableValue<B> b = StableValue.of();
A() {
// IllegalStateException: Recursive initialization
// is not supported
b.orElseSet(B::new);
}
}
public static class B {
static final StableValue<A> a = StableValue.of();
B() {
a.orElseSet(A::new);
}
}
public void main(String[] args) {
A myA = new A();
}
}
IllegalStateExceptionは、クラスAのコンストラクタが安定値A.bを初期化しようとしたときにスローされます。この安定値は、A.bに依存するB.aに依存します。
安定リストおよび安定int関数
安定リストは変更不可能なリストであり、安定値の配列によって支えられ、IntFunctionに関連付けられます。リスト内の要素に初めてアクセスすると、その要素がパラメータとして索引を使用する安定リストのIntFunctionで計算された値で初期化されます。同じ索引を持つ要素に追加の時間アクセスすると、再度計算されるかわりに、その値が取得されます。
StableValue.list(int, IntFunction)ファクトリ・メソッドを使用して安定リストを作成する場合、最初の引数でそのサイズを指定します。2番目の引数でその要素の計算方法を指定します。
次の例では、Productsオブジェクトのプールを作成します。これにより、異なるProductsが様々なアプリケーション・リクエストを処理し、ロードをプール全体に分散できます。プール内のProductsオブジェクトごとに安定値を作成するかわりに、この例では安定リストを作成します。異なるアプリケーション・リクエストをシミュレートするために、この例では2つの仮想スレッドを作成して起動します。これらの各スレッドで、Productsオブジェクトが安定リストで初期化されます。
public class StableListExample {
private final static int POOL_SIZE = 20;
final static Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(StableListExample.class.getName()));
static final List<Products> PRODUCTS
= StableValue.list(POOL_SIZE, _ -> new Products());
public static Products products() {
long index = Thread.currentThread().threadId() % POOL_SIZE;
logger.get().info("Obtaining Products from stable list, index " + index);
return PRODUCTS.get((int)index);
}
public static void main(String[] args) {
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
System.out.println("Thread ID: " + Thread.currentThread().threadId());
products().printProducts();
};
try {
// name "worker-0"
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " terminated");
// name "worker-1"
Thread t2 = builder.start(task);
t2.join();
System.out.println(t2.getName() + " terminated");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
例では、次のような出力が表示されます:
Thread ID: 22
Apr 24, 2025 6:59:16 PM StableListExample products
INFO: Obtaining Products from stable list, index 2
Apr 24, 2025 6:59:16 PM Products printProducts
INFO: Printing products...
worker-0 terminated
Thread ID: 26
Apr 24, 2025 6:59:16 PM StableListExample products
INFO: Obtaining Products from stable list, index 6
Apr 24, 2025 6:59:16 PM Products printProducts
INFO: Printing products...
worker-1 terminated
次の文は、20個のProductsオブジェクトを保持する安定リストを作成します。安定リストの要素はまだ初期化されていないことに注意してください。
static final List<Products> PRODUCTS
= StableValue.list(POOL_SIZE, _ -> new Products());
PRODUCTS.get(int index)の最初の呼出しでは、次のラムダ式を使用して、indexにある安定値のコンテンツが初期化されます:
_ -> new Products()
ヒント:
アンダースコア文字(_)は無名変数です。この変数は、宣言されているが使用可能な名前を持たない変数を表します。変数が宣言後に使用されていないことを示すのに役立ちます。『Java Platform, Standard Edition Java言語更新』の無名変数およびパターンに関する項を参照してください。
同じ索引を持つPRODUCTS.get(int index)のその後の呼出しでは、その要素のコンテンツをすぐに取得します。
安定int関数は、安定リストと同様に機能します。安定int関数は、intパラメータを取得し、それを使用して結果を計算する関数で、そのパラメータ値のバッキング安定値記憶域によってキャッシュされます。そのため、同じintパラメータを使用して安定関数を追加の時間でコールすると、その結果は再度計算されるのではなく取得されます。
安定リストと同様に、StableValue.intFunction(int, IntFunction)ファクトリ・メソッドを使用して安定int関数を作成する場合、最初の引数にそのサイズを指定します。2番目の引数でその要素の計算方法を指定します。
次の例は、安定int関数を使用することを除いて、StableListExampleと同様です。コードの大幅な変更が強調表示されます:
public class StableIntFunctionExample {
private final static int POOL_SIZE = 20;
final static Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(StableIntFunctionExample.class.getName()));
static final IntFunction<Products> PRODUCTS
= StableValue.intFunction(POOL_SIZE, _ -> new Products());
public static Products products() {
long index = Thread.currentThread().threadId() % POOL_SIZE;
logger.get().info("Obtaining Products from stable int function, index " + index);
return PRODUCTS.apply((int)index);
}
public static void main(String[] args) {
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
System.out.println("Thread ID: " + Thread.currentThread().threadId());
products().printProducts();
};
try {
// name "worker-0"
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " terminated");
// name "worker-1"
Thread t2 = builder.start(task);
t2.join();
System.out.println(t2.getName() + " terminated");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
安定リストと安定int関数の違いは、それと対話する方法です。リストのように安定マップと対話します。値には、Map::get(int)メソッドを使用してアクセスします。また、List::subListやList::reversed()などの値を変更しないリスト・インタフェースのメソッドをコールできます。IntFunctionのように安定関数と対話します。Function::applyメソッドを使用して値を計算します。安定int関数に格納された計算値のリスト・ビューは取得できませんが、文またはIntFunction<R>タイプのラムダ式を必要とするパラメータとして使用できます。
安定マップおよび安定関数
安定マップは、作成時に指定したキーを持つ変更不可能なマップです。関数にも関連付けられています。最初にキーの値にアクセスすると、そのキーをパラメータとして使用する安定マップの関数で計算された値で初期化されます。キーの値に追加の時間にアクセスすると、その値は再度計算されるのではなく取得されます。
次の例では、バイナリ表現の先頭のゼロの数をカウントして、整数の二進対数を計算します。この例では、2の最初の6乗の二進対数のみを計算できることに注意してください。StableValue.map(Set, Function)ファクトリ・メソッドを使用して安定マップを作成する場合は、最初の引数で安定マップの関数が受け入れることができるすべての使用可能なパラメータを指定する必要があります。2番目の引数でキーの値の計算方法を指定します。
public class Log2StableMap {
private static final Set<Integer> KEYS =
Set.of(1, 2, 4, 8, 16, 32);
private static final Function<Integer, Integer> LOG2_FUNCTION =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Map<Integer, Integer> LOG2_SM =
StableValue.map(KEYS, LOG2_FUNCTION);
public static void main(String[] args) {
System.out.println("Log base 2 of 16 is " + LOG2_SM.get(16));
System.out.println();
LOG2_SM.entrySet()
.stream()
.forEach(e -> System.out.println(
"Log base 2 of " + e.getKey() + " is " + e.getValue()));
}
}
例では、次のような出力が表示されます:
Log base 2 of 16 is 4
Log base 2 of 4 is 2
Log base 2 of 16 is 4
Log base 2 of 2 is 1
Log base 2 of 1 is 0
Log base 2 of 8 is 3
Log base 2 of 32 is 5
安定関数は、安定マップと同様に機能します。安定関数は、パラメータを取得し、それを使用して結果を計算する関数で、そのパラメータ値のバッキング安定値記憶域によってキャッシュされます。そのため、同じパラメータを使用して安定関数を追加の時間でコールすると、その結果は再度計算されるのではなく取得されます。
次の例は、安定マップではなく安定関数を使用することを除いて、Log2StableMapと同様です。コードの大幅な変更が強調表示されます:
public class Log2StableFunction {
private static final Set<Integer> KEYS =
Set.of(1, 2, 4, 8, 16, 32);
private static final Function<Integer, Integer> LOG2_FUNCTION =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Function<Integer, Integer> LOG2_SF =
StableValue.function(KEYS, LOG2_FUNCTION);
public static void main(String[] args) {
System.out.println("Log base 2 of 16 is " + LOG2_SF.apply(16));
System.out.println();
KEYS.stream()
.forEach(e -> System.out.println(
"Log base 2 of " + e + " is " + LOG2_SF.apply(e)));
}
}
ここでも、この例では、2の最初の6乗の二進対数のみを計算できることに注意してください。StableValue.function(Set, Function)ファクトリ・メソッドを使用して安定関数を作成する場合は、最初の引数で、安定関数の関数が受け入れることができるすべての使用可能なパラメータを指定する必要があります。2番目の引数で安定関数の結果を計算する方法を指定します。
安定リストおよび安定int関数と同様に、安定マップと安定関数の違いは、対話方法です。マップのように安定マップと対話します。値には、Map::get(Object key)メソッドを使用してアクセスします。また、Map::values()やMap::entrySet()などの値を変更しないマップ・インタフェースのメソッドをコールできます。関数のように安定関数と対話します。Function::applyメソッドを使用して値を計算します。安定関数に格納された計算値のコレクション・ビューは取得できませんが、文またはFunction<T,R>タイプのラムダ式を必要とするパラメータとして使用できます。
定数畳込み
定数畳込みはコンパイラの最適化であり、定数式は実行時ではなくコンパイル時に評価されます。JITコンパイラは、finalと宣言されたフィールド、静的フィールド、レコードおよび非表示クラスに存在する安定値に対して定数畳込みを実行することがあります。定数畳込みでは、JITコンパイラによって出力されるマシン・コードにかわりに値を埋め込むことができるため、メモリーから値をロードする必要がなくなります。定数畳込みは、一連の最適化の最初のステップであり、同時にパフォーマンスを大幅に向上させることができます。
スレッド・セーフティ
StableListExampleおよびStableIntFunctionExampleの例に示されているように、安定値はスレッド・セーフです。安定値の内容は、最大1回設定することが保証されます。競合スレッドが安定値を設定するために競合している場合、1つの更新のみが成功し、他の更新は安定値が設定されるまでブロックされます。