16 遅延定数
遅延定数は、コンテンツと呼ばれる単一のデータ値を保持するLazyConstant型のオブジェクトです。遅延定数のコンテンツは不変です。JVMは、遅延定数を定数として扱います。これにより、finalフィールドと同じパフォーマンス最適化が可能になります。ただし、遅延定数のコンテンツを初期化するタイミングについては、finalフィールドに比べて柔軟性が高くなります。
ノート:
これはプレビュー機能です。プレビュー機能は、設計、仕様および実装が完了したが、永続的でない機能です。プレビュー機能は、将来のJava SEリリースで、異なる形式で存在することもあれば、まったく存在しないこともあります。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。『Preview Language and VM Features』を参照してください。遅延定数の背景情報については、次を参照してください...
ロガーを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 LazyConstant<Logger> logger =
LazyConstant.of(
() -> Logger.getLogger(Orders.class.getName())
);
public void printOrders() {
logger.get().info("Printing orders...");
}
}静的ファクトリ・メソッドLazyConstant.of(Supplier)は、最初はコンテンツを保持しない遅延定数を作成します:
private final LazyConstant<Logger> logger =
LazyConstant.of(
() -> Logger.getLogger(Orders.class.getName())
);logger.get().info("Printing orders...");遅延値のコンテンツがすでに初期化されている場合、LazyConstant.get()はそれをかわりに取得します。
遅延定数の集約と構成
アプリケーション内で複数の遅延定数を集約することで、起動時間を短縮できます。また、他の遅延定数から遅延定数を作成することもできます。前述の例のOrdersは、ロガー・コンポーネントを遅延定数に格納する方法を示しています。次の例では、Locations、CustomersおよびOrdersコンポーネントを独自の遅延定数に格納します。
public class LoggingExample {
static final LazyConstant<Locations> LOCATIONS = LazyConstant.of(Locations::new);
static final LazyConstant<Customers> CUSTOMERS = LazyConstant.of(Customers::new);
static final LazyConstant<Orders> ORDERS = LazyConstant.of(Orders::new);
public static Locations locations() {
return LOCATIONS.get();
}
public static Customers customers() {
return CUSTOMERS.get();
}
public static Orders orders() {
return ORDERS.get();
}
public void main(String[] args) {
locations().printLocations();
customers().printCustomers();
orders().printOrders();
}
}
アプリケーションのコンポーネントを遅延定数に格納することで、その起動時間を大幅に短縮できます。アプリケーションでは、すべてのコンポーネントが事前に初期化されなくなります。この例では必要に応じてコンポーネントを初期化します。
Ordersコンポーネントは、ロガーに独自の内部遅延定数を使用しています。特に、遅延定数ORDERSは、遅延定数Orders.loggerに依存します。ORDERSがまだ存在しない場合は、依存するOrders.loggerが最初に作成されます。
循環依存関係がある場合は、次の例に示すIllegalStateExceptionがスローされます:
public class LazyConstantCircularDependency {
public static class A {
static final LazyConstant<B> b = LazyConstant.of(B::new);
A() {
// java.lang.IllegalStateException: Recursive
// invocation of a LazyConstant's computing function
b.get();
}
}
public static class B {
static final LazyConstant<A> a = LazyConstant.of(A::new);
B() {
a.get();
}
}
public void main(String[] args) {
A myA = new A();
}
}
クラスAのコンストラクタが、遅延定数A.bを初期化しようとすると、IllegalStateExceptionがスローされます。この遅延定数は、B.aに依存しており、B.aはA.bに依存しています。
遅延リスト
遅延リストは、遅延定数の配列を基盤とし、IntFunctionに関連付けられている、変更不可能なリストです。リスト内の要素に初めてアクセスすると、その要素がパラメータとして索引を使用する遅延リストのIntFunctionで計算された値で初期化されます。同じ索引を持つ要素に追加の時間アクセスすると、再度計算されるかわりに、その値が取得されます。
List.ofLazy(int, IntFunction)ファクトリ・メソッドを使用して遅延リストを作成する場合、最初の引数でそのサイズを指定します。2番目の引数でその要素の計算方法を指定します。
次の例では、Ordersオブジェクトのプールを作成します。これにより、異なるOrdersオブジェクトが異なるアプリケーション・リクエストに対応できるようになり、負荷がプール全体に分散されます。この例では、プール内の各Ordersオブジェクトに対して遅延定数を作成するのではなく、遅延リストを作成します。異なるアプリケーション・リクエストをシミュレートするために、この例では2つの仮想スレッドを作成して起動します。これらの各スレッドで、遅延リスト内のOrdersオブジェクトが初期化されます。
public class LazyListExample {
private final static int POOL_SIZE = 20;
static final List<Orders> ORDERS
= List.ofLazy(POOL_SIZE, _ -> new Orders());
public static Orders orders() {
long index = Thread.currentThread().threadId() % POOL_SIZE;
return ORDERS.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());
orders().printOrders();
};
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: 23
Oct 01, 2025 6:39:15 PM Orders printOrders
INFO: Printing orders...
worker-0 terminated
Thread ID: 27
Oct 01, 2025 6:39:15 PM Orders printOrders
INFO: Printing orders...
worker-1 terminated
次の文は、20個のOrdersオブジェクトを保持する遅延リストを作成します。遅延リストの要素はまだ初期化されていないことに注意してください。
static final List<Orders> ORDERS
= List.ofLazy(POOL_SIZE, _ -> new Orders());
ORDERS.get((int)index)の最初の呼出しでは、indexにある遅延定数のコンテンツを次のラムダ式で初期化します:
_ -> new Orders()
ヒント:
アンダースコア文字(_)は無名変数です。この変数は、宣言されているが使用可能な名前を持たない変数を表します。変数が宣言後に使用されていないことを示すのに役立ちます。『Java Platform, Standard Edition Java言語更新』の無名変数およびパターンに関する項を参照してください。
その後、同じ索引でORDERS.get((int)index)を呼び出すと、その要素のコンテンツが即座に取得されます。
遅延マップ
遅延マップとは、作成時に指定したキーを持つ変更不可能なマップのことです。関数にも関連付けられています。最初にキーの値にアクセスすると、そのキーをパラメータとして使用する遅延マップのFunctionで計算された値で初期化されます。キーの値に追加の時間にアクセスすると、その値は再度計算されるのではなく取得されます。
次の例では、バイナリ表現の先頭のゼロの数をカウントして、整数の二進対数を計算します。この例では、2の最初の6乗の二進対数のみを計算できることに注意してください。Map.ofLazy(Set, Function)ファクトリ・メソッドを使用して遅延マップを作成する場合は、最初の引数で遅延マップのFunctionが受け入れることができるすべてのパラメータを指定する必要があります。2番目の引数でキーの値の計算方法を指定します。
public class LazyMapExample {
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_LM =
Map.ofLazy(KEYS, LOG2_FUNCTION);
public static void main(String[] args) {
// This is eligible for constant folding; log base 2 of 16 is 4
System.out.println("Log base 2 of 16 is " + LOG2_LM.get(16));
System.out.println();
LOG2_LM.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 32 is 5
Log base 2 of 8 is 3
Log base 2 of 16 is 4
Log base 2 of 4 is 2
Log base 2 of 2 is 1
Log base 2 of 1 is 0
定数畳込み
定数畳込みはコンパイラの最適化であり、定数式は実行時ではなくコンパイル時に評価されます。JITコンパイラは、finalと宣言されているフィールド、静的フィールド、レコードおよび非表示クラスに存在する遅延定数に対して定数畳込みを実行することがあります。定数畳込みでは、JITコンパイラによって出力されるマシン・コードにかわりに値を埋め込むことができるため、メモリーから値をロードする必要がなくなります。定数畳込みは、一連の最適化の最初のステップであり、同時にパフォーマンスを大幅に向上させることができます。たとえば、LazyMapExampleの式LOG2_LM.get(16)は、定数畳込みの対象となります。
スレッド・セーフティ
LazyListExampleの例に示すように、遅延定数はスレッドセーフです。遅延定数の内容は、最大で1回設定されることが保証されます。競合するスレッドが遅延定数を設定するために競合している場合、1つの更新のみが成功し、その他の更新は遅延定数が設定されるまでブロックされます。