S-JIS[2023-09-24/2024-03-22] 変更履歴

ScopedValue(Java21〜22)

Java21〜22のScopedValue(プレビュー版)について。


概要

Java20でインキュベーター版だったScopedValueが、2023/9/19にリリースされたJava21でプレビュー版になった。

ScopedValueは、マルチスレッドで呼ばれるメソッドがサブルーチンのメソッドとオブジェクトを共有できる仕組みを提供する。

サブルーチンを呼び出す際にオブジェクトを共有しようと思ったら、普通はサブルーチンの引数で渡すが、大量にあるといちいち書くのも大変になる。
オブジェクトをフィールドで保持して共有しようと思っても、マルチスレッドで呼ばれる場合はインスタンスフィールドでは保持できないので、従来ならThreadLocalを使うことになる。
しかしThreadLocalは「インスタンスが生きている間は値を保持し続けるような大きなスコープ」で使われるものなので、メソッド呼び出しの間だけ共有するような目的には重い。(特に仮想スレッドで使おうとすると、ThreadLocalの問題点が大きくなってくるらしい)
そこで、軽量なScopedValueが用意された。


ScopedValueはJava21〜22ではプレビュー版の機能なので、この機能を使いたい場合はコンパイル時にjavacコマンドに--enable-previewを付ける必要があり、
また、実行時にjavaコマンドに--enable-previewを付ける必要がある。
JShellで試す場合もjshellコマンドに--enable-previewを付ける。

> javac --enable-preview --release 21 Example.java
> java --enable-preview Example
> java --enable-preview --source 21 Example.java

ScopedValueクラスの使用例。

ScopedValueオブジェクトは、まずstaticフィールドで定義しておく。
(ScopedValueはjava.langパッケージなので、import無しで使える)

public class ScopedValueExample {

    private static final ScopedValue<StringBuilder> SB = ScopedValue.newInstance();
    void main() {
        var sb = new StringBuilder();

        ScopedValue.where(SB, sb)	// whereで、ScopedValueに値をマッピングする
            .run(() -> {	// runで、関数を実行する
                sub1();
                sub2();
            });

        System.out.println(sb);
    }
    private void sub1() {
        StringBuilder sb = SB.get(); // マッピングされている値が取得できる
        sb.append("sub1.");
    }

    private void sub2() {
        StringBuilder sb = SB.get(); // マッピングされている値が取得できる
        sb.append("sub2.");
    }
}

ScopedValueのwhereメソッドで、「staticフィールドのScopedValueオブジェクト」と値(この例ではStringBuilder)を指定する。
その後のrunメソッドで関数を実行する。
この関数が実行されている間は、ScopedValueオブジェクトから値(この例ではStringBuilder)が取得できる。

なお、ScopedValueのwhereメソッドを呼び出さずにScopedValueオブジェクトからgetしようとすると、NoSuchElementExceptionが発生する。


2つ以上のScopedValueを使いたい場合は、whereメソッドを複数呼び出す。

import java.util.*;

public class ScopedValueExample {

    private static final ScopedValue<StringBuilder> SB = ScopedValue.newInstance();
    private static final ScopedValue<List<String>> LIST = ScopedValue.newInstance();
    void main() {
        var sb = new StringBuilder();
        var list = new ArrayList<String>();

        ScopedValue.where(SB, sb).where(LIST, list).run(() -> {
            sub1();
            sub2();
        });

        System.out.println(sb);
        System.out.println(list);
    }
    private void sub1() {
        StringBuilder sb = SB.get();
        sb.append("sub1.");

        List<String> list = LIST.get();
        list.add("sub1");
    }

    private void sub2() {
        StringBuilder sb = SB.get();
        sb.append("sub2.");

        List<String> list = LIST.get();
        list.add("sub2");
    }
}

staticなwhereメソッドでなくSB.where(sb)みたいにして値をバインドすればいいのにと思ったんだけど、そうすると複数の値をバインドできないから、現在のような方法になってるんだろう。


ScopedValue関連のメソッド

ScopedValueのstaticメソッド

メソッド 説明
ScopedValue<T> newInstance() ScopedValueオブジェクトを生成する。 private static final ScopedValue<StringBuilder> SB = ScopedValue.newInstance();
ScopedValue.Carrier where(ScopedValue<T> key, T value) ScopedValueオブジェクトの値を設定する。
(その後、runメソッド等を呼び出す)
ScopedValue.where(SB, new StringBuilder())
void runWhere(ScopedValue<T> key, T value, Runnable op) 値を設定して実行する。
where(key, value).run(op)と同じ。
ScopedValue.runWhere(SB, new StringBuilder(), () -> {
  SB.get().append("abc");
});
R callWhere(ScopedValue<T> key, T value, Callable<R> op) 値を設定して実行し、opの戻り値を返す。
where(key, value).call(op)と同じ。
String s = ScopedValue.callWhere(SB, new StringBuilder(), () -> {
  return SB.get().append("abc").toString();
});
R getWhere(ScopedValue<T> key, T value, Supplier<R> op) 値を設定して実行し、opの戻り値を返す。
where(key, value).get(op)と同じ。
String s = ScopedValue.getWhere(SB, new StringBuilder(), () -> {
  return SB.get().append("abc").toString();
});

ScopedValue.Carrierのメソッド

ScopedValue.Carrierは、ScopedValueのwhereメソッドから返されるクラス。

メソッド 説明
ScopedValue.Carrier where(ScopedValue<T> key, T value) ScopedValueオブジェクトの値を設定し、新しいScopedValue.Carrierを返す。 var c1 = ScopedValue.where(SB, new StringBuilder());
var c2 = c1.where(LIST, new ArrayList<>());
void run(Runnable op) opを実行する。 c1.run(() -> {
  SB.get().append("abc");
});
R call(Callable<R> op) opを実行し、その戻り値を返す。 String s = c1.call(() -> {
  return SB.get().append("abc").toString();
});
R get(Supplier<R> op) opを実行し、その戻り値を返す。 String s = c1.get(() -> {
  return SB.get().append("abc").toString();
});
T get(ScopedValue<T> key) ScopedValueにマッピングされている値を取得する。 StringBuilder sb = c1.get(SB);

ScopedValueのインスタンスメソッド

ScopedValueのインスタンスメソッド(get等)は、主にrunメソッド等の関数内で使用する。

メソッド 説明
T get() マッピングされている値を取得する。
マッピングされていない場合はNoSuchElementExceptionが発生する。
StringBuilder sb = SB.get();
boolean isBound() 値がマッピングされているかどうかを返す。 boolean bound = SB.isBound();

 

T orElse(T other) マッピングされている値を取得する。
マッピングされていない場合はotherを返す。
StringBuilder sb = SB.orElse(null);
T orElseThrow(Supplier<Throwable> exceptionSupplier) マッピングされている値を取得する。
マッピングされていない場合はexceptionSupplierで生成される例外をスローする。
StringBuilder sb = SB.orElseThrow(Exception::new);

プレビュー機能へ戻る / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま