2024/12/12

Null-Restricted Typeとオブジェクト初期化の変更
 で、ValhallaのValue Classってどうなったの? その3

このエントリーをはてなブックマークに追加

本エントリーはJava Advent Calendarの12日目です。昨日はmackey0225さんのイレイジャってなんじゃ?でした。

 

Project Valhallaを紹介するエントリーも3回目になりました。

本エントリーではNull-Restricted Type(Null非許容型)について紹介していきます。いわゆるNon-Nullです。

Null-Restricted Typeに関するJEPは2つありますが、現状はドラフトなので番号がついていません。

 

Null非許容型にまつわる小史

Javaの開発者であれば誰もが1度は遭遇したことがあるNull Pointer Exception例外ですが、これを防ぐための取り組みが行われてきました。

ここではOpenJDKおよびJCPによる標準APIでの取り組みについて簡単に紹介します。

 

型アノテーションを使ったDefect Detection

変数にNullを許容するかしないかをアノテーションで修飾する取り組みは、多くライブラリやフレームワークでも導入されてきました。

たとえば、IntelliJ IDEAの設定で[Compiler]の項の1つに[Add runtime assertions for notnull-annotatedmethods and parameters]があり、その[Configure annotations...]をクリックすると、どのライブラリ/フレームワークのアノテーションを使用するか選択できます(下図参照)。

 

この設定ダイアログを見ると、AndroidやJakarta EEなどが@NonNullアノテーションもしくは@NotNullアノテーションを導入していることが分かります。

たとえば、メソッド引数にnullを禁止したいのであれば、次のように書けます。

    String readContext(@NonNull String filename) throws IOException {
        ...
    }

 

ところが、このアノテーションだと書けないことがあります。たとえば、リストの要素にnullを許さない場合はどうでしょう。これを解決するためにJava 8で導入されたのが、型アノテーション(JSR 308 Annottations on Java Type)です。

型アノテーションは型に対してアノテーションで修飾します。たとえば、リストの要素にnullを許さないという場合は次のように記述できます。

    List<@NonNull String> texts = ...;

 

そして、この型アノテーションを使用してNull非許容性を表そうとしたのが、JSR 305 Annotations for Software Defect Detecctionです。

このJSRのスペックリードはFindBugsの作者のBill Pughだったのですが、Bill Pughに連絡がとれなくなり、JSRも中断してしまいました。ご存じの方もいらっしゃると思いますが、FindBugsの開発が停滞してしまったのもこの頃です。

同様にJSR 308の型アノテーションを使って@NonNullを表そうとしたのが静的解析ツールのChecker Frameworkなのですが、こちらもそこまで流行らず...

うまく活用すればよかったのですが、標準にならなかったのが痛かったのが型アノテーションを使ったNull非許容性でした。

 

Optional

OptionalもJava 8で導入されました。

Optional自体はNull許容性を表すというよりは、値の有無を扱うために使われるクラスです。

しかし、値がないことをnullで表す場合が多かったため、Optionalを使うことでnullの使用を避けることができました。

ところが、Optional型を使ったとしても次のように書けてしまうのが...

    Optional<String> option = null;

 

つまり、値の有無を扱うことはできても、自分自身のNull非許容性は表せないのです。

 

ということで、Null非許容性を表すための取り組みはあったものの、成功したとはいえないのがJavaの現状でした。

 

Null-Restricted Type/Nullable Type

さて、Project ValhallaのNull-Restricted Typeです。

今までのNullに対する取り組みは、ソフトウェアの堅牢性を高めるためのものでした。これに対しValhallaのNull-Restricted Typeはパフォーマンス向上のためという大きな違いがあります。

前回、説明したValueクラスの平坦化やスカラー化は、行われるためのいくつかの条件があります。そのうちの1つが、値にnullが入らないことです。

Valueオブジェクトはプリミティブ型のようにふるまいますが、プリミティブ型の変数にはnullが値として入ることがありません。もし、平坦化やスカラー化で値を埋め込む時にnullが入るかもしれないのであれば、それを示すためのフラグなどが必要になります。しかし、それではせっかくの最適化の効果が低くなってしまいます。

なので、nullを許さないというのが最適化の条件になっているわけです。

 

ただし、Null非許容性はValueクラスでなくても有用です。そこで、Value Classとは独立して仕様を策定しようというのがJEPのNull-Restricted and Nullable Typesです。

そして、ValueクラスのNull非許容性はNull-Restricted Value Class Typesで仕様策定されます。

 

Null-Restricted Type/Nullable Typeの書き方

Null非許容/Null許容型の変数は次のように記述します。

    // null非許容
    String! nonnullText = "...";

    // null許容
    String? nullableText = "...";

 

他の言語でもNon-NullとNullableに!と?を使うことが多いので、理解しやすいですね。!と?はnullnessマーカーと呼ばれます。

nullnessマーカーはジェネリクスの型パラメータでも使用することができます。

    // null非許容
    class Foo<T!> { ... }

    // null許容
    class Bar<T?> { ... }

    // null非許容
    Foo<String!> foo = ...;

    // null許容
    Bar<String?> bar = ...;

 

!や?を指定していない型は未指定(Unspecified)です。未指定、つまり従来の型については仕様を変更していないので、nullが入ることもあります。実質的には?と未指定は同じような動作になりますが、型としては異なります。

また、nullnessの型を変換することもできます。Foo!をFoo?に代入するようなwide変換はOKです。しかし、Foo?をFoo!に代入するようなnarrow変換の場合、コンパイル時に警告が出るようです。

ただし、このJEPに対応するEalry Accessがないので、実際にどのような警告が出るのか、実行させるとどうなるのかなどは、よく分かりません。キャストすればいいのか、nullチェックをした後でないと代入できないのかなどは、Early Accessが出たら確かめてみたいと思います。

 

配列の初期化

Null-Restrictedな変数は、変数の宣言時に初期化を行う必要があります。ただし、クラスのフィールドであれば、コンストラクターやイニシャライザーでも初期化できます。

ここで困るのが配列です。要素も含めて初期化する必要があるからです。

たとえば、"a", "b", "c"を要素に持つString!の配列であれば、次のように書けます。

    String![] texts = new String![] { "a", "b", "c" };

では、初期値として""で埋めた、長さ10の配列はどうでしょう。また、配列のインデックスを使った初期化はどうでしょう? もちろん、Stream APIを使えば書けますが、それではちょっとおおげさですね。

現状のJEPのドラフトでは以下の書き方が提案されていますが、あくまでも現状であり、文法については変わる可能性も高いのですが、とりあえずこういうことが書けるようなことが考えられています。

    String![] texts1 = new String![10] { "" };
    String![] texts2 = new String![10] { i -> "s" + i };

 

この他にもメソッドをオーバーロードする場合、nullnessの違いだけではオーバーロードできないなど、いろいろとルールがありますが、実際にやってみないと具体的にどのようになるのかがJEPだけではよく分からないことが多々あります。

Early Accessが出て、JEPもドラフトではなく正式なものになったら、再度取り上げてみたいと思います。

 

オブジェクト初期化の変更

クラスのフィールドがNull-Restrictedな型の場合、宣言時に初期化するか、コンストラクターもしくはイニシャライザーで初期化する必要があります。

では、次に示すコードは実行したらどのようにふるまうでしょう。

フィールドの初期化ははまりどころが多いので、よくクイズになるところですね。Javaのクイズといえば、JavaOneの名物セッションだったJoshua BlochとNeal GafterによるJava Puzzlersです。

短いコードを提示して実行したらどうなるかを4択で選ぶというセッションなのですが、彼らのウィットに富んだセッションはさくらばもとても影響を受けています。

ということで、ここでもJava Puzzlersをまねて、実行したらどうなるかを4択で選んでみてください。

class Cat {
    String meow = "Meow";

    Cat() {
        meow = ((Lion)this).roar;
    }
}

class Lion extends Cat {
    final String roar;

    Lion() {
        roar = "Roar";
    }
}

public class DoLionMeow {
    public static void main(String... args) {
        System.out.println(new Lion().meow);
    }
}

選択肢は以下の4つ

  1. Meow
  2. Roar
  3. null
  4. 例外発生

 

ちなみに、Meowはネコの鳴き声(ニャーオ)で、Roarはライオンの鳴き声(ガオー)です。

このDoLionMeowクラスでは、Lionオブジェクトを生成して、そのスーパークラスであるCatクラスのフィールドのmeowを表示させています。

Catクラスのコンストラクターでは、サブクラスのLionのroarをmeowに代入しています。roarはLionのコンストラクターで"Roar"を代入しています。

 

さて、どうでしょう。

答えは 3. の null です。

 

それほど難しくはないですよね。

roar変数はfinalなので一度しか初期化できません。しかし、実際には初期化する前の状態があり、その時の値はnullになります。

そして、Lionクラスのコンストラクターでは省略されていますが、super()をコールしているということです。つまり、Lionクラスのコンストラクターは省略しないで記述すると、次のようになります。

    Lion() {
        super();
        roar = "Roar";
    }

 

このため、roarを初期化する前にCatクラスのコンストラクターがコールされてしまい、初期化されていないroarにアクセスしてしまっているということです。

初期化していないのでroarの値はnullになり、meowに代入するので、結果的にnullが表示されてしまいます。

 

ここで重要なのはfinal変数でも、初期化前の状態にアクセスできてしまうということです。もし、meowの型がString!だったらどうでしょう。nullはとらないはずなのに、実際はnullになってしまうのは問題です。

これを解決するために、オブジェクトの初期化を変更するというのがJEP 492: Flexible Constructor Bodiesです。

JEP 492はProject Valhallaではなく、Javaの言語仕様をアップデートするProject Amberで策定されています。Value ClassやNull-Restricted Typeとは独立に仕様策定できるので、Value Classにさきがけてアップデートしてしまおうということなのかもしれません。

JEP 492はJava 24で3rd Previewになっているので、次のLTSのJava 25に標準で取り込まれる可能性が高いです。

 

Lionクラスのように今までのコンストラクターは、必ず先頭でスーパークラスのコンストラクターを呼び出していました。デフォルトコンストラクター以外のコンストラクターをコールするのであれば、明示的にsuper(...)をコールする必要がありますが、デフォルトコンストラクターであれば記述を省略できます。

このため、スーパークラスからはサブクラスの初期化していないフィールドにアクセスできてしまいます。

そこで、JEP 492ではsuper(...)をコールする前にフィールドの初期化を行えるように言語仕様を変更しています。

 

とはいえ、super(...)をコールする前にどういう処理でもできるわけではありません。たとえば、thisは使うことはできません。他にもルールはありますが、要するにフィールドの初期化以外の処理をsuper(...)の前に記述しないということが重要です。

また、デフォルトコンストラクターのsuper()を省略した場合は、現在の使用と同じくコンストラクターの先頭でコールされます。

 

さて、DoLionMeowクラスで"Roar"を出力させるためには、Lionクラスのコンストラクターを次のように記述すればよいことが分かります。

    Lion() {
        roar = "Roar";
        super();
    }

 

ただし、JEP 492はPreview JEPなので、コンパイルや実行する時にはオプションの--enable-previewが必要です。

 

ところで、前述したJava Puzzlersのセッションではフィールドの初期化に関するパズルは必ず1問は出題される頻出分野だったのですが、JEP 492が導入されるとそれらのパズルは通用しなくなってしまいますね。まぁ、どちらにしろずっと昔の話なので、どうでもいいといえばどうでもいいのですけど。

 

最後に

3回に渡ってValue Classに関連したトピックを紹介してきました。

Value Classを作成するのは簡単ですが、最適化されることを考慮して使う必要があります。

サイズが小さいことと、フィールドがNull-Restrictedであることが最適化の条件です。このように考えると、Recordクラスで記述していたデータで、サイズが小さければValue Classにするというのがいいと思います。

とはいうものの、いつからValue Classが使えるようになるのかはまだまだ分かりません。Project Valhallaが発足して10年。ここまで待ったのですから、もうちょっとだとは思いますが気長に待ちましょう!

2024/12/02

Valueクラスによる最適化
 で、ValhallaのValue Classってどうなったの? その2

このエントリーをはてなブックマークに追加

本エントリーはJava Advent Calendarの2日目です。昨日はHatanoさんのこんにちは、世界でした。

 

さて、前回のエントリーに続き、Project ValhallaのValue Classについて紹介していきます。

本エントリーでは、Value Classを使用した場合に可能になる最適化について説明します。

 

前回のエントリーで紹介したようにValue Classの導入の背景にあったのが、ヒープ使用効率の最適化にあります。

そこで説明したのが、配列の領域に参照ではなく、直接データを埋め込む手法です。この最適化を平坦化(Flattering)と呼ぶようです。まずは、この平坦化から紹介していきましょう。

 

なお、Value Classはまだ策定中の仕様なので、今後変化する可能性があります。本エントリーで説明していることも変わる可能性が高いので、その点はご了承ください。

 

平坦化

Value ClassのオブジェクトをIdentityオブジェクトと同じようにヒープに配置するのではなく、Valueオブジェクトをフィールドとして持つオブジェクトのフィールド領域に埋め込んでしまうのが平坦化です。

前回は配列で説明しましたが、value record Point(double x, double y){}のような小さなValue Classであれば、その配列であるPoint[]にPointオブジェクトが保持すべきxとyを直接埋め込むこと最適化が可能です。

とはいっても、配列なんか使わないからなぁ... と思いますよね。

配列ではなくてリストで平坦化してくれればと思いますよね。でも、ほとんどの場合、リストといえばArrayListクラスを使っているはず。

ArrayListクラスのArrayは配列のこと。ArrayListクラスが内部で保持している配列が平坦化できれば恩恵は大きいはず。

ただ、ArrayListクラスが内部で保持しているのはObjectクラスの配列なのが気になります。今のジェネリクスは、型パラメータで指定された型の配列を作成することができません。

これができれば、平坦化することも可能なはず。というようなことをProject Valhallaの人たちが考えていないわけがないので、今後何らかの進展があると予想しているのですが、どうなんでしょうね。

また、平坦化は配列以外にも適用されます。たとえば、以下のようなレコードはどうでしょう。

value record Point(double x, double y) {}
    
record Rectangle(Point topLeft, Point bottomRight) {}

 

RectangleレコードクラスはValueクラスのPointオブジェクトを2つフィールドに保持します。Valueクラスであれば、フィールドに参照を保持させるのではなく、直接値を保持できるようになります。

さらにRectangleレコードクラスがValueクラスであれば... というように考えていくこともできるはずです。

ただし、平坦化が常に行われるとは限りません。Value ClassがPreview機能で提供されたとしても、当初は最適化される部分は少ないはずです。リリースが進むにつれ、徐々に最適化の範囲が増えていくことが予想されます。

 

スカラー化

もう1つの最適化がスカラー化(Scalarized)です。スカラー化というと多目的計画法で使う言葉だと思っていたのですが、JVMの最適化でも使うんですね。

それはそうとして、以下のようなコードを考えてみます。

    record Score(int score) {}
    
    record Adder(int sum) {
	Adder() { this(0); }

	Adder add(int v) {
	    return new Adder(sum + v);
	}
    }

    int calcTotal(List<Score> scores) {
	Adder adder = new Adder();

	for (var s: scores) {
	    adder = adder.add(s.score());
	}

	return adder.sum();
    }

 

通常は意識しないとは思いますが、Javaのコードはjavacコンパイラでバイトコードに変換され、JVMはバイトコードを実行します。

バイトコードの実行にはスレッドごとにJava Stackという特殊なスタックが作成されます。スタックにはメソッドごとにフレームが積まれます。このフレームにはオペランドスタックというスタックとローカル変数用の領域を持っており、これらを利用してバイトコードを実行します。

オペランドスタックは実行中の状態を保持させるスタックで、演算やメソッドコールはこのオペランドスタックに積まれた値に対して行われます。

ローカル変数領域も実際に使用する時には、オペランドスタックにロードし、処理の結果は再びローカル変数領域にストアされます。

ローカル変数領域もプリミティブ型の値であれば直接保持されますが、参照型の値の場合はヒープに存在するオブジェクトへの参照が保持されます。

つまり、上記のcalTotalメソッドの場合、scores変数はListオブジェクトへの参照、adder変数はAdderオブジェクトへの参照が保持されるわけです。

このcalcTotalメソッドのバイトコードは以下のようになります。

  int calcTotal(java.util.List<Score>);
    descriptor: (Ljava/util/List;)I
    flags: (0x0000)
    Code:
      stack=2, locals=5, args_size=2
         0: new           #7                  // class Adder
         3: dup
         4: invokespecial #9                  // Method Adder."<init>":()V
         7: astore_2
         8: aload_1
         9: invokeinterface #10,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
        14: astore_3
        15: aload_3
        16: invokeinterface #16,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
        21: ifeq          48
        24: aload_3
        25: invokeinterface #22,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        30: checkcast     #26                 // class Score
        33: astore        4
        35: aload_2
        36: aload         4
        38: invokevirtual #28                 // Method Score.score:()I
        41: invokevirtual #32                 // Method Adder.add:(I)LAdder;
        44: astore_2
        45: goto          15
        48: aload_2
        49: invokevirtual #36                 // Method Adder.sum:()I
        52: ireturn

全体を解説することはしませんが、注目していただきたいところは色付きにしました。

オレンジの0から7の行はAdderオブジェクトを生成して、ローカル変数の[2]に保存しているバイトコードになります。

インデックス0にはthis、インデックス1には引数のListオブジェクトの参照が保持されており、その後にAdderオブジェクトの参照が保持されるわけです。

赤で示した35から44がJavaのコードでいうところのforループの内部の処理に当たります。

aload_2でローカル変数[2]からAdderオブジェクトの参照をオペランドスタックに積み、次のaloadでローカル変数[4]をスタックに積んでいます。このインデックス4には、Scoreオブジェクトの参照が保持されています。

その後の38のinvokevirtualがコメントにあるようにScoreクラスのscoreメソッドをコールしています。その結果はそのままスタックに積まれます。この時点でスタックにはAdderオブジェクトとscoreメソッドの戻り値のint値が積まれています。

そして、41のinvokevirtualでAdderクラスのaddメソッドをスタックに積まれたint値を引数にコールします。addメソッドの戻り値は新たに生成されたAdderオブジェクトで、スタックに積まれるので、44のastore_2でローカル変数[2]に保存されます。

 

 

このように、forループの内部では毎回Adderオブジェクトを生成し、ローカル変数のオブジェクト参照を更新するということを繰り返します。また、そのオブジェクトが保持している値はやはり毎回アクセスする必要があります。

毎回のオブジェクト生成や、値の取得処理が省略できるのであれば、パフォーマンが向上します。

もし、メソッド内で使用していたオブジェクトが、戻り値などでメソッドの外に逃げ出さないのであれば、この最適化をすることができます。

メソッドから逃げ出さないというのは、当該メソッド以外の部分でオブジェクト参照するということです。オブジェクトにどこから参照されるか分からないので、たとえValueオブジェクトであっても通常のIdentityオブジェクトと同じようにヒープにオブジェクトを配置しなければなりません。

逆に、オブジェクトがメソッド内だけで使われるのであれば、Valueオブジェクトをヒープに作るのではなく、Valueオブジェクトが保持する値をローカル変数領域に直接保持させてしまえばいいわけです。

 

 

ローカル変数領域に値を直接保持させることで、オブジェクト生成や参照の張替えが不要になります。

繰り返しになりますが、この最適化はValueオブジェクトがメソッド内にとどまっていることが条件になります。このため、オブジェクトがメソッド内だけで使用されているかどうかを調べる必要があります。

これをエスケープ解析(Escape Analysis)と呼びます。オブジェクトがメソッドの外に逃げ出さないかどうかを解析するということですね。エスケープ解析でオブジェクトが逃げ出さないと分かれば、スカラー化以外にも最適化が可能になります。

 

ここでは2種類の最適化を紹介しましたが、Valueクラスが提供された時にはじめから両方の最適化が行われるとは限りません。まずはValueクラスを使えるようになり、そこから徐々に最適化が導入されていくことが予想されます。

また、最適化を行うにはエスケープ解析でオブジェクトが逃げ出さないことが条件になりますが、他にもいくつか条件があります。その1つにnullの扱いがあります。

たとえば、Valueオブジェクトが保持する値にnullが紛れてしまうと、平坦化もスカラー化もできなくなってしまいます。

このため、Project Valhallaでは、null非許容性とnull許容性、つまりNon-NullとNullableを導入することになりました。

Non-NullとNullableは以前から要望がありましたが、まさかProject Valhallaによって仕様策定されることになるとは思いもよりませんでした。

そこで、次のエントリーではNon-Null/Nullableと、それに関連してオブジェクト初期化処理の変更について紹介する予定です。

2024/11/10

で、ValhallaのValue Classってどうなったの? その1
(JJUG CCC 2024 Fall)

このエントリーをはてなブックマークに追加

10月27日にJJUG CCC 2024 Fallが開催されました。

久しぶりのベルサール新宿グランド。前回までの野村コンファレンスプラザ新宿に比べると、部屋も増えて、参加者も大幅に増えたようです。

で、さくらばはProject Valhallaで策定されているValue Classについてプレゼンしてきました。資料はこちら。

なお、Value Classはまだ策定中の仕様なので、今後変化する可能性があります。本エントリーで説明していることも変わる可能性が高いので、その点はご了承ください。

Value Classを説明する前に、そもそもProject Valhallaとは何なのか?

Project ValhallaはJavaの型システムを見直して、整理するためのOpenJDKのサブプロジェクトです。

Valhallaとは北欧神話に出てくる主神オーディンの宮殿のことです。戦士の魂が最終的に集められるのがValhallaで、日本人的な感覚だとあまり縁起のいい場所ではないような気がするんですけど、どうなんでしょう。

ちなみに、上の資料の表紙の背景にある道は、スゥエーデンのストックホルムにあるヴァルハラ通りです。今年の2月にストックホルムで開催されたJfokusに参加したので、ついでにValhallaにも行ってみたわけですw

そんなこんなで、資料の背景の写真はすべてストックホルムで撮った写真を使ってます。

 

さて、Project Valhallaです。

Valhallaでは型の再整理を行っているのですが、主な論点としては以下の4つがあります。

Value ClassがValhallaのメインとなる論点で、この後説明していきます。

Value Classの導入過程で必要となったのが、Null-Restricted/Nullable Typeです。

Specialized Genericsというのは、ジェネリクスの型パラメータにプリミティブ型も使用できるようにしようというものです。

Primitive拡張とSpecialized Genericsは、Value ClassとNull-Restricted/Nullable Typeに比べると、仕様策定にまだまだ時間がかかりそうなので、ここでは触れません。

 

Value Class導入の背景

Value Classの解説をする前に、まずValue Classの導入の背景について説明しましょう。

ご存じの通り、Javaには2種類の型があります。一方がプリミティブ型、もう一方が参照型です。

クラスでオブジェクトを作ってというのは、すべて参照型ですね。また、Javaでは配列も参照型となります。

Javaの言語仕様的にはプリミティブ型と参照型には以下のような違いがあります。

最後の初期化はプリミティブ型ではデフォルト値(たとえば数値型であれば0)があり、初期化しなくてもデフォルト値で使用できます。一方の参照型では必ずnewをしてオブジェクトを生成して初期化しなくてはならないということです。参照型変数のデフォルト値としてnullがありますが、これは変数のデフォルト値であってオブジェクトのデフォルト値ではないです。

言語仕様的なこのような違いはありますが、Valhallaで着目しているのは2つの型がヒープでどのように扱われているかということです。

たとえば、doubleの配列を考えてみます。

配列も参照型のオブジェクトなので、ヒープに生成する場合、オブジェクトヘッダーとフィールド用の領域を確保します。

プリミティブ型の値を配列に格納する場合、フィールド用の領域に直接値が書きこまれます。

一方、参照型の配列の場合、フィールド用の領域には要素のオブジェクトへの参照が格納されます(これが参照型と呼ばれる理由です)。

たとえば、2つのdoubleの要素を持つPointレコードの場合を示したのが、以下の図です。

Pointオブジェクトがヒープ上のどこに配置されるのかについては、JVMまかせでユーザーは指定できません。このため、Pointオブジェクトが離れた位置に配置されることもあります。

 

CPUのメモリアクセス

ここで、CPUがどのようにメモリにアクセスするかを紹介しておきましょう。

CPUの内部ではALU (Arithmetic Logic Unit、演算ユニット)が演算を行うのですが、そのためのデータはレジスターに格納します。

レジスターは高速ですが小容量なので、他のデータはメインメモリーに配置されます。必要に応じて、メインメモリーからレジスターにデータをロードします(もちろん、その逆方向もあります)。

しかし、メインメモリーは速度が遅いため、メインメモリーに直接アクセスするとCPUがアイドル状態になってしまいます。このため、現代のCPUではレジスターとメインメモリーの間にはキャッシュを配置しています。

その構成を表したのが以下の図です。

キャッシュはL1, L2, L3の3レベルあり、数字が少ないほど高速ですが、容量は少なくなります。

L1にデータがあれば数クロックでアクセスできますが、キャッシュにないデータをロードする場合桁違いに遅くなるわけです。

これをキャッシュミスと呼びます。

データにアクセスする時に、なるべくキャッシュミスが起こらないようにするのが、パフォーマンスを向上させる秘訣になります。

メインメモリーからキャッシュにデータをロードする時は、1つ1つのデータではなく、ある程度まとまった単位(チャンク)でロードします。これは、あるデータを使用する時、その近くにあるデータにもアクセスする傾向があるからです。たとえば、ループで配列をイテレートする場合などですね。

 

参照型配列のヒープ使用効率

前節で一緒に使うデータをなるべく近くに配置すれば、キャッシュミスが発生しないことを説明します。ところが、参照型の配列だとどうでしょう。

Pointオブジェクトはヒープ上で固まって配置されるとは保証されません。つまり、キャッシュミスを引き起こす可能性が高いということです。

デフォルトで使用されるG1GCの場合、メモリを領域で区分し、Young領域とOld領域に分けられます。そして、新しいオブジェクトは基本的にYoung領域に配置されます。

このため、上記のPointオブジェクトがヒープ上で遠く離れた位置に作られる可能性は少ないのですが、複数のYoung領域に分かれて生成させることはあるかもしれません。このような場合に、キャッシュミスを引き起こしてしまうわけです。

では、どうすればよいでしょう?

プリミティブ型の値と同じようにデータを直接フィールド領域に格納してしまえばいいということです。つまり、下図のようになります

しかし、参照型を使用する限り、このようなデータ格納を行うことができません。

そこで、プリミティブ型に近い新たな型の導入が望まれたわけです。

キーとなるのは、"Codes like a class, works like an int"です。

クラスのように書けるけども、intのようにふるまうということです。

Project Valhallaでは、これを実現させるために10年に渡って議論を続けてきました。数年前までは、新たに様々な型を導入するという複雑な実現方法が提案されていました。しかし、あまりにも複雑すぎました。

そこで、去年ぐらいから、もっとシンプルな方法が検討され、やっと議論が収束してきたのです。

そして、新たに提案されたのがValue Classです。

Value Typeではなく、Value Classだというのがポイントです。

つまり、新たな型を導入するのではなく、既存の参照型の枠組みの中で特殊なクラスを導入することでCode like a class, works like an intが実現できるということです。

では、そのValue Classというのは、どのようなクラスなのでしょう?

 

Value Class

Value Classの仕様はJEP 401: Value Classes and Objectsに記述されています。

端的にいうと、Value Classをインスタンス化したオブジェクト(Value Object)にはIdentityがありません。といわれても、「Identityって何?」と思いますよね。私もそうでした。

このIdentity、Java Language SpecificationにもJVM Specificationにも明確な定義はありません。

Identityはオブジェクトを区別するために使われるオブジェクトの名前もしくはアドレスのようなものです。

具体的な値としてはSystem.identityHashCodeメソッドが返す値になります(もしくはオーバーライドしていない場合のObject.hashCodeメソッド)。

この値は、==でオブジェクト同士を比較する場合に使用されます。

また、Identityでオブジェクトを区別することが、オブジェクトの状態変更を可能にします。また、synchronizedを使用したモニタロックもIdentityを利用して実現しています。

しかし、なぜIdentityなのでしょうか?それはオブジェクトのヘッダーに関係があります。

オブジェクトヘッダー

オブジェクトヘッダーは、ヒープ上に存在するオブジェクト領域の先頭にあります。実をいうと、オブジェクトヘッダーは、JVMの実装依存でJVM Specificationには定義されていません。ここでは、OpenJDKの64bitのHotSpot VMでのオブジェクトヘッダーについて紹介します。

HotSpot VMのオブジェクトヘッダーはマークワードとクラスワードの2つのパートから構成されます。マークワードはハッシュ値、GC Age、Tagからなります。GC AgeはGCを何度経てきたかを表す回数を示します。また、Tagはマークワードがポインターで上書きされてしまうことがあるので、それを区別するために使われます。

一方のクラスワードはクラスへのポインターが格納されます。

こう見てみると、GC AgeとTagを除けば、ヘッダーによってクラスとオブジェクトを区別するための情報が格納されていることが分かります。クラスはともかく、Identityがないということはオブジェクトヘッダーがなくても大丈夫ということです。

このことから、オブジェクトヘッダーを省略してしまって、そのオブジェクトをフィールドに持つクラスに直接データを埋め込むことが可能であることを示しています。

 

ちなみに、オブジェクトヘッダーはそれなりにサイズが大きいので、小さいオブジェクトだとヘッダーの方が大きいということが起こります。そこで、Project Lilliputでオブジェクトヘッダーを小さくする仕様を策定しています。

ちょうど、次のJava 24で、LilliputのJEP 450: Compact Object HeadersがExperimentalとして導入予定です。

 

あらためてValue Class

identityが分かったところで、あらためてValue Classの定義について説明しましょう。

Value ClassはIdentityがないクラスですが、もう1つの特徴としてイミュータブルであることがあります。

Value Classではフィールドを定義すると、そのフィールドはすべて暗黙的にfinalになります。

また、Identityがないことにより、Identityを使用していた操作はできません。

たとえば、オブジェクトの比較を行う==演算は、オブジェクトの同一性ではなく、フィールドの等価性の結果を返します。つまり、equalsメソッドで比較する場合と同様になるということです。

他にも、synchronizedを使用したモニターロックや、参照を使用する弱参照(WeakReference)、ファントム参照(PhantomReference)なども使用できません。

 

Value Classの書き方

では、Value Classをどのように定義すればよいのでしょう。

これはとても簡単でclassもしくはrecordの前にvalueをつければよいだけです。

value record Point(double x, double y) {}

value class Rectangle {
    Point topleft;
    Point bottomright;

    Rectangle(Point tl, Point br) {
	topleft = tl;
	bottomright = br;
    }
}

Record Classはもともとイミュータブルなので、valueを付加しても特に問題なくコンパイルできます。

通常のクラスの場合、状態を変更するようなコードがあるとコンパイルエラーになります。

たとえば、Rectangleクラスを以下のようにセッターを追加してコンパイルしてみます。

value class Rectangle {
    Point topleft;
    Point bottomright;

    Rectangle(Point tl, Point br) {
	topleft = tl;
	bottomright = br;
    }

    public void setTopLeft(Point tl) {
	topleft = tl;
    }

    public void setBottomRight(Point br) {
	bottomright = br;
    }
}

 

> javac --release 23 --enable-preview Rectangle.java
Rectangle.java:13: エラー: final変数topleftに値を割り当てることはできません
        topleft = tl;
        ^
Rectangle.java:17: エラー: final変数bottomrightに値を割り当てることはできません
        bottomright = br;
        ^
ノート: Test.javaはJava SE 23のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
エラー2個

前述したように、Value Classのフィールドは暗黙的にfinalになるため、そこに再代入しているためコンパイルエラーになっています。

なお、ここでコンパイルに使用しているJDKはjdk.java.netで公開されているValhallaのEarly Access版です。

 

Vlaue Classが作成できるようになったので、Value Classの特徴の1つでもある==での比較を行ってみましょう。

通常のクラス(Value Classに対応してIdentity Classと呼びます)とValue Classで、JShellを使用して比較してみました。

jshell> record IDPoint(double x, double y) {}
|  次を作成しました: レコード IDPoint

jshell> var idp1 = new IDPoint(1, 2)
idp1 ==> IDPoint[x=1.0, y=2.0]

jshell> var idp2 = new IDPoint(1, 2)
idp2 ==> IDPoint[x=1.0, y=2.0]

jshell> idp1 == idp2
$4 ==> false

jshell> idp1.equals(idp2)
$5 ==> true

jshell> value record VPoint(double x, double y) {}
|  次を作成しました: レコード VPoint

jshell> var vp1 = new VPoint(1, 2)
vp1 ==> VPoint[x=1.0, y=2.0]

jshell> var vp2 = new VPoint(1, 2)
vp2 ==> VPoint[x=1.0, y=2.0]

jshell> vp1 == vp2
$9 ==> true

jshell> vp1.equals(vp2)
$10 ==> true

jshell>

Identity Classだと、フィールドの値が同一のオブジェクトであっても、==はIdentityが同じかどうかを調べるので、falseになります。その一方、Value ClassではIndentityがなく、フィールドの同値性を調べるので、==の結果はtrueになっています。もちろん、equalsメソッドで比較してもtrueです。

 

では、継承についてはどうでしょう。

Value Classは、コンクリートクラスの場合、finalクラスになるためサブクラスを作ることはできません。ただし、Value Classの抽象クラスであればサブクラスを作ることができます。

jshell> value class A {}
|  次を作成しました: クラス A

jshell> value class B extends A {}
|  エラー:
|  final Aからは継承できません
|  value class B extends A {}
|                        ^
|  エラー:
|  The concrete class A is not allowed to be a super class of the value class B either directly or indirectly
|  value class B extends A {}
|  ^------------------------^

jshell> class C extends A {}
|  エラー:
|  final Aからは継承できません
|  class C extends A {}
|                  ^

jshell> abstract value class X {}
|  次を作成しました: クラス X

jshell> value class Y extends X {}
|  次を作成しました: クラス Y

jshell> class Z extends X {}
|  次を作成しました: クラス Z

jshell>

最後のコードは抽象クラスのValue Classを継承してIdentity Classを作れるということです。まぁ、作れたとしても使うことはないでしょうけど。

もちろん、インタフェースを実装したValue Classを作ることは可能です。

jshell> interface I {}
|  次を作成しました: インタフェース I

jshell> value class J implements I {}
|  次を作成しました: クラス J

jshell>

ここで一度Value Classについてまとめておきましょう。

Value Classは以下のような特徴を持つクラスです。

  • identityのないクラス
  • イミュータブル
  • finalクラス
  • ==はフィールドの状態の比較
  • identityに依存した操作は不可
    • synchronizedを使用したモニタロック
    • 弱参照、ファントム参照などの参照

書き方に関しては以下のようになります。

  • classもしくはrecortdの前にvalueを付加して宣言
  • 抽象クラスでもValue Classにすることが可能
  • 抽象Value Classのサブクラスを定義可能
  • インタフェースの実装も可能

 

長くなってしまったので、最適化やNull-Restricted Typeについては次のエントリーで紹介していきます。

2024/09/17

JEPでは語れないJava 23

このエントリーをはてなブックマークに追加

毎度おなじみ半年ぶりのJavaのアップデートです。

Java 23はLTSのちょうど中間のリリースということもあって、それほど変化があるわけではないです。

Java 23のJEPは12ありますが、Previewばかり。PreviewでないStandard JEPは3つですがAPIの変更が伴うものはありません。

Java 23のJEPは以下の通り。

  • 455: Primitive Types in Patterns, instanceof, and switch (Preview)
  • 466: Class-File API (Second Preview)
  • 467: Markdown Documentation Comments
  • 469: Vector API (Eighth Incubator)
  • 473: Stream Gatherers (Second Preview)
  • 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
  • 474: ZGC: Generational Mode by Default
  • 476: Module Import Declarations (Preview)
  • 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
  • 480: Structured Concurrency (Third Preview)
  • 481: Scoped Values (Third Preview)
  • 482: Flexible Constructor Bodies (Second Preview)

Standard JEPの1つめ。JEP 467はJavadocにマークダウンが使えるというものです。

これは地味にうれしいかも。ただ、マークダウンにする時にはスラッシュ3つというのはちょっと面倒かもしれません。

IDEがマークダウンのJavadocに対応して、ショートカットでスラッシュ3つを簡単に使えるようになってほしいですね。

2つめのJEP 471は、UnsafeクラスのヒープではないネイティブメモリまわりのAPIをDeprecated for RemovalにするというJEPです。通常の用途ではUnsafeクラスは使わないとは思いますが、昔は高速化のためにヒープではなくUnsafeを使って直接メモリにアクセスしていたフレームワークやライブラリがそれなりにあったのです。

しかし、FFMが導入されたので、Unsafeを使わずにメモリアクセスできるようになったので、ようやく削除できるようになったということですね。ただ、ほんとに削除されるのがいつになるのかは微妙なところです。

Standard JEPの最後のJEP 474は、ZGCのデフォルトを世代別ZGCに変更するというものです。もともとZGCは世代別GCではなかったのですが、Java 21で世代別GCをサポートするようになりました。これに伴い、ZGCのデフォルトが世代別GCの方に変わるというものです。

ただ、これはあくまでもZGCの話で、JVMのデフォルトのGCの話ではないことに注意が必要です。

 

残りのJEPはすべてPreviewです。

JEP 455はパターンマッチングにプリミティブ型が使えるようになるというJEPです。ちょっとおもしろいのが、プリミティブ型の値と型のcaseを同じswitch式の並べて書けるところですね。まぁ、そんなに使うことはないと思いますが...

ちなみに、JEP 455は1st Previewなので、次のLTSであるJava 25に入らないんじゃないかなぁ。

 

JEP 466はバイト操作を行うためのClas-File APIです。バイトコードを直接編集してしまうわけですが、一時期ちょっとだけはやったAOPなどで使われる技術です。Class-File APIはAOPというよりは、ラムダ式の実行時に動的にクラス生成するなどJVM内部での用途が主目的のようです。

 

JEP 469 Vector APIは8回目のPreview。Project Valhallaが導入されるまで、ずーーーーっとPreviewのままです。7thからの変更点もありません。

 

JEP 473 Stream Gatherersはストリームの中間処理をもうちょっとどうにかしようというJEP。簡単にいえば、終端処理のcollectと同様のことを中間処理でもできるようにしましょうというAPIです。

これで、今までのストリームではできなかった移動平均なんかも簡単に書けるようになります。

JEP 473は2nd Previewで、すでにPreviewが外されたJEP 485が提案されているので、Java 24で正式導入ということになりそうです。

 

JEP 476 Module Import Declarationsは、インポート文をクラス単位で書くのではなくて、moduleで書けるようにしてしまいましょうというJEPです。これができると、import module java.base;で基本的なAPIは全部使えます。

これはかなり便利になりますけど、使っているクラスがどのパッケージで定義されているのか調べるにはIDEの力に頼らなければいけないという負の側面がなきにしもあらず...

JEP 476は1st Previewなので、Java 25にはまにあわないかもしれません。

 

JEP 477 Implicitly Declared Classes and Instance Main Methodsはmainメソッドを含むクラスを書かなくても、mainメソッドだけ書けばいいんじゃないというJEPです。

ちなみに、このJEPでは動的にクラスを作成していますが、その生成はバイトコード操作ではなくて、実行中にJavaのコードを動的に生成して、コンパイルとクラスロードも行うという実装になっています。

 

JEP 480 Structured ConcurrencyとJEP 481 Scoped ValuesはProject LoomでVirtual Threadsと一緒に仕様策定されていたAPIです。

両方とも3rd Previewですが、Java 23でStandardになると予想していたので、外してしまいました😱

JEP 480は複数の非同期タスクの結果を待つような処理を簡単に書けるというAPIです。CompletableFutureクラスを使えば同様の処理は書けるのですが、CompletableFutureクラスの宣言的な書き方ではなくて、手続き的な記述でも使えるよというのがポイントでしょう。

JEP 481はThreadLocalクラスの置き換えになるAPIです。ThreadLocalクラスはいろいろと問題があり、特にVirtual Threadのように多くのスレッドを使うようになるとその問題が顕現しやすくなります。

これを解決するために、JEP 481ではScopedValueクラスを導入しています。

Scoped Valueは4th PreviewがJEPのドラフトに上がっているので、Java 25に間に合うかは微妙なところです。

 

最後のJEP 482 Flexible Constructor Bodiesは、Java 22の時のJEP 447 Statements before super()の名前が変更されたJEPです。

JEP 447ではコンストラクターで、親クラスのsuper()をコールする前に処理を書けるようにしましょうというJEPでした。JEP 482ではこれに加えて、super()をコールする前に子クラスのフィールドの初期化も行えるようにしましょうというJEPになりました。

これはクラスの初期化のスキームの大きな変更なのですが、使う側からするとフィールドを先に初期化したいというニーズはほとんどないはずです。

JEPには書いてないのですが、これはProject ValhallaのVlue Classに関連しているのです。Value Classの露払いとなるJEPなのでした。

 

軽くJEPを説明したところで、本題のAPIの変更について紹介していきましょう。

 

例によって、セキュリティ関連のAPIは省略します。本バージョンでも、java.baseモジュール以外にもAPIの変更はありますが、使用頻度が低いAPIであるため、解説を省略します。

 

廃止になったAPI

Java 23では4つのクラス、5つのメソッドが削除されました。ただし、削除された4つのクラスはjava.managementモジュールのJMXに関するクラスなので、ここでは省略します。

 

メソッド

Java 22でもThreadクラスのメソッドが削除されましたが、Java 23でもスレッド関連のメソッドが削除されてインす。

  • java.lang.Thread.resume()
  • java.lang.Thread.suspend()
  • java.lang.ThreadGroup.resume()
  • java.lang.ThreadGroup.suspend()
  • java.lang.ThreadGroup.stop()

これらのメソッドはJava 14でforRemovalがtrueになっていたので、とうとう削除されたという感じですね(ThreadGroup.stopメソッドだけはJava 16です)。

それ以前からスレッドのresume/suspendは使うべきではないメソッドだったので、ようやくです。

ThreadクラスのforRemovalがtrueのメソッドは残り2つ。1つはstopメソッドですが、ThreadGroupクラスで削除されたので、Threadクラスも近いうちに削除されるような気がします。

もう1つはcheckAccessメソッドですが、こちらもいつ削除されても不思議はない感じ。

 

廃止予定のAPI

Java 23では6つのメソッドと2つのコンストラクタが廃止予定に追加されました。

  • java.io.ObjectOutputStream.PutField.write(ObjectOutput out)
  • java.net.DatagramSocketImpl.getTTL()
  • java.net.DatagramSocketImpl.setTTL(byte ttl)
  • java.net.MulticastSocket.getTTL()
  • java.net.MulticastSocket.setTTL(byte ttl)
  • java.net.MulticastSocket.send(send(DatagramPacket p, byte ttl)

PutFieldクラスのwriteメソッドの代わりは、ObjectOutputStreamクラスのwriteFieldsメソッドです。

DatagramSocketImplクラスとMulticastSocketクラスのTTLに関するメソッドは、TTLではなくTimeToLiveを使うようにします。たとえば、getTTLメソッドではなく、getTimeToLiveメソッドを使用します。

最後のMulticastSocketクラスのsendメソッドは、MulticastSocketクラスの親クラスのDatagramSocketクラスで定義されているsend(DatagramPacket p)メソッドを使用するようにします。TimeToLiveを指定するにはDatagramSocketクラスのsetOptionメソッドを使用します。

 

削除予定のコンストラクタは以下の2つです。

  • java.net.Socket(InetAddress host, int port, boolean stream)
  • java.net.Socket(String host, int port, boolean stream)

このメソッドはDatagramSocketクラスが提供される前に使われていたメソッドなのですが、DatagramSocketクラスを使うようにしましょうということです。

 

なお、java.desktopモジュールのjava.bean.beancontextパッケージもforRemovalがtrueになりました。benacontextパッケージでは18のクラスが定義されていますが、すべてforRemoval=trueになっています。

また、java.desktopモジュールに含まれるSwingのBasicSliderUI()コンストラクタも削除予定に追加されています。

 

追加/変更されたAPI

いつもの通り、Preview JEPに関するAPI変更はここでは省略します。ということで、Class-File APIなどはまた別の機会に。

 

java.base/java.ioパッケージ

JEP 477でjava.io.IOクラスが導入されるのですが(Preview APIなので、ここでは省略します)、そのIOクラスの実装で使わるConsoleクラスに多くのメソッドが追加されました。

 

Consoleクラス

Consoleクラスには7つのメソッドが追加されました。

  • Console format(Locale locale, String format, Object... args)
  • Console print(Object obj)
  • Console printf(Locale locale, String format, Object... args)
  • Console println(Object obj)
  • Console readLine(Locale locale, String format, Object... args)
  • Console readPassword(Locale locale, String format, Object... args)
  • Console readln(String prompt)

追加されたメソッドの多くは、メソッドをオーバーロードしてLocaleを指定できるようにしたものです。だいたい使い方は分かりますよね。

 

java.base/java.langパッケージ

Java 22のPreview JEPだったString Templatesがやり直しになったので、StringTemplateクラスは削除されています。また、上述したThreadクラスとThreadGroupクラスのメソッドが削除されています。

また、JEP 481 Scoped ValueのAPIが変更になっていますが、ここでは省略します。

 

java.base/java.lang.foreignパッケージ

Java 22でStandard JEPになったFFMですが、メソッドが2つ追加されました。

 

MemorySegmentインタフェース

MemorySegmentインタフェースで定義されるメソッドが1つ追加されました。

  • long maxByteAlignment()

メモリーセグメントの最大アライメントを返すメソッドです。しかし、この値を何らかの処理に使うというよりは、MemoryLayoutインタフェースのbyteAlignmentメソッドで得られる値と比較してメモリーセグメントの最大アライメントの方が小さいときには例外処理をするという使い方になります。

 

SymbolLookupインタフェース

SymbolLookupインタフェースで定義されるメソッドが1つ追加されました。

  • default MemorySegment findOrThrow(String name)

SymbolLookupインタフェースは、基本的にはfindメソッドでライブラリ内のシンボルのアドレスを探索するために使用します。findメソッドの戻り値の型はOptionalクラスで、見つからなかった場合はOptionalオブジェクトでどうにかしていました。

これに対し、Java 23で追加されたfindOrThrowメソッドを使用すると、見つからなかった場合にNoSuchElementException例外をスローします。

個人的にはOptionalクラスで見つからなかった場合に対処する方がいいとは思いますが、お好みで使い分けてください。

 

java.base/java.lang.reflectパッケージ

毎度のことですが、新しいリリースを表す定数が追加されています。

 

ClassFileFormatVersion列挙型

Java 23に対応する定数の追加です。

  • ClassFileFormatVersion RELEASE_23

 

java.base/java.lang.runtimeパッケージ

プリミティブ型の値を他の型に変換する場合に、正確であるかを調べるExactConversionsSupportクラスが追加されました。

 

ExactConversionsSupportクラス

ExactConversionsSupportクラスで定義しているメソッドは以下の21メソッドです。いずれもstaticメソッドになります。

  • static boolean isDoubleToByteExact(double n)
  • static boolean isDoubleToCharExact(double n)
  • static boolean isDoubleToFloatExact(double n)
  • static boolean isDoubleToIntExact(double n)
  • static boolean isDoubleToLongExact(double n)
  • static boolean isDoubleToShortExact(double n)
  • static boolean isFloatToByteExact(float n)
  • static boolean isFloatToCharExact(float n)
  • static boolean isFloatToIntExact(float n)
  • static boolean isFloatToLongExact(float n)
  • static boolean isFloatToShortExact(float n)
  • static boolean isIntToByteExact(int n)
  • static boolean isIntToCharExact(int n)
  • static boolean isIntToFloatExact(int n)
  • static boolean isIntToShortExact(int n)
  • static boolean isLongToByteExact(long n)
  • static boolean isLongToCharExact(long n)
  • static boolean isLongToDoubleExact(long n)
  • static boolean isLongToFloatExact(long n)
  • static boolean isLongToIntExact(long n)
  • static boolean isLongToShortExact(long n)

ExactなConvertって何だろうという感じですが、これはコードを見てみればすぐに意図が分かります。たとえば、isIntToByteExactメソッドの実装を見てみましょう。

    public static boolean isIntToByteExact(int n) {
        return n == (int)(byte)n;
    }

キャストが2つつなげて書いてあります。つまり引数のintの値をbyteにキャストして、その後にintに戻した時に元の値と同じかどうかを調べているわけです。もし、変換の時に情報が欠落するような変換であれば、2回キャストすると元の値と異なってしまうはずです。これが、Exactかどうかということです。

 

動作は分かりましたが、なぜこんなクラスが今になって追加されたのですね。理由は単純でJEP 455 Primitive Types in Patterns, instanceof, and switchのためです。

パターンマッチングでプリミティブ型が使えるようになりましたが、その時に値を正確に変換できるかどうかが重要になるからです。たとえば、下のコードで考えてみましょう。

    int x = ...;
	
    var y = switch (x) {
        case 0 -> "0";
        case byte a -> "Byte " + a;
        case int b -> "Int " + b;
    };

このコードでは、xの値が-128から127であれば、byteのcaseにマッチします。それを超える範囲、たとえば128だとbyteの範囲を超えるので、intのcaseにマッチします。

つまり、正確に変換が行えるのであれば、型が異なっていてもマッチするわけです。この正確な変換ができるかどうかをチェックするためにExactConversionsSupportクラスが使われるのです。

型の変換に関してはJEP 455にも記述があるので、参考にしてみてください。

 

java.base/java.netパッケージ

Java 22でInet4Address/Inet6Addressクラスのファクトリメソッドが追加されましたが、Java 23ではInet4Addressクラスにファクトリメソッドがさらに追加されました。

 

Inet4Addressクラス

Inet4AddressクラスにアドレスをPosixのリテラルで指定できるファクトリメソッドが追加されました。

  • static Inet4Address ofPosixLiteral(String posixIPAddressLiteral)

ofLiteralメソッドでは10進数でアドレスを表記しますが、ofPosixLiteralメソッドでは8進数や16進数も使用することができます。

 

java.base/java.textパッケージ

数値をフォーマットするNumberFormatクラスと、そのサブクラスにフォーマットの厳密さを指定するメソッドが追加されました。

 

NumberFormatクラス

NumberFormatクラスのパースはデフォルトでは寛大になっています。これに対し、厳密なパースに関するメソッドが追加されました。

  • boolean isStrict()
  • void setStrict(boolean strict)

NumberFormatクラスでは、これらのメソッドをコールするとUnsupportedOperationException例外がスローされます。

厳密なパース処理は、以下の3種類のサブクラスで使用することができます。

  • ChoiceFormat
  • CompactNumberFormat
  • DecimalFormat

それぞれのクラスのparseメソッドのAPIドキュメントに厳密な場合について記述されているので、参考になさってください。まぁ、それほど使うとは思わないですけどw

 

java.base/java.timeパッケージ

時点を表すInstantメソッドにメソッドが1つオーバーロードされました。

 

Instantクラス

Instantクラスには時間量を調べるuntilメソッドがありましたが、オーバーロードされています。

  • Duration until(Instant endExclusive)

既存のuntilメソッドはもう一方の時点をTemporalオブジェクトで指定し、戻り値はlongで表されます(どの時間量なのかは第2引数で指定します)。

これはちょっと使いにくいので、時点をInstantオブジェクトで表し、戻り値は時間間隔を表すDurationオブジェクトで表されるuntilメソッドのオーバーロードが追加されたわけです。

 

その他

なんとAPIの変更はこれだけなのです。なのですが、他にちょっとだけ気になる変更があったので、それも一緒に紹介しておきます。

 

COMPATロケールプロバイダーの廃止

ロケールプロバイダーってなんだという感じですが、ロケールのデータベースのようなものです。

Javaでは歴史的経緯から3種類のロケールプロバイダーを提供していました。しかし、Java 9からは世界的な標準であるCommon Locale Data Repository (CLDR)がデフォルトになっています。

そして使われなくなった残り2つのロケールプロバイダー(JREとCOMPAT)が削除されることになりました。

Java 21から、JREかCOMPATを使っていると警告が表示されていたのですが、早々に削除されることになりました。

詳しくはJava Bug Systemの JDK-8325568 をご覧ください。

 

標準Docletの変更

DocletというのはJavaのソースコードからJavadocを生成するためのツールです。

JEP 467でJavadocの見直しがあったためなのかどうか分かりませんが、標準Docletが変更されJavadocの見た目が変わりました。

具体的には下図のように左側にサイドバーが出るようになっています。以前のように階層をたどるためのサイドバーではなく、右側に表示しているクラスやインタフェースの目次的なサイドバーになっています。

しかし、これが微妙なんですよね。メソッドの一覧がソートされておらず、書いてある順になっているので探しにくいのです。Method Summaryの表のようにソートされないですかねぇ。

 

というわけで、Java 23のAPI変更について紹介してきました。

Java 23のAPIの変更は少なかったのですが、次のJava 24では大幅にAPIが追加されそうです。

というのも、すでにPreviewではなくなったJEP 484 Class-File APIやJEP 485 Stream Gatheresが提案されているからです。この2つのJEPはまだターゲットリリースが記述されていませんが、Java 24になるのは既定路線でしょう。

2024/03/19

JEPでは語れないJava 22

このエントリーをはてなブックマークに追加

毎度おなじみ半年ぶりのJavaのアップデートです。

Java 22は、LTSであるJava 21の次のバージョンですが、意外と新機能盛りだくさんです。

Java 22のJEPは以下の12。しかも、スタンダードJEPが4もあります。

  • 423: Region Pinning for G1
  • 447: Statements before super(...) (Preview)
  • 454: Foreign Function & Memory API
  • 456: Unnamed Variables & Patterns
  • 457: Class-File API (Preview)
  • 458: Launch Multi-File Source-Code Programs
  • 459: String Templates (Second Preview)
  • 460: Vector API (Seventh Incubator)
  • 461: Stream Gatherers (Preview)
  • 462: Structured Concurrency (Second Preview)
  • 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
  • 464: Scoped Values (Second Preview)

注目すべきは、長らくIncubatorやPreviewだったJEP 454。FFMと省略して呼ぶことがおおいですが、Project Panamaのメインとなる機能です。

JNIの代わりに、ネイティブコードをコールしたり、ヒープ外のメモリにアクセスするためのAPIです。

モジュールはjava.baseで、パッケージはjava.lang.foreignになります。

APIなので、本来であれば本エントリーでも取り上げるのですが、ちょっと量が多いですし、差分を紹介してもしかたありません。そこで、別エントリーで使い方についてまとめて紹介する予定です。

ちなみに、同じくProject Panamaで仕様策定しているVector APIはまだIncubatorのままですが、次のバージョンで正式にリリースされるのではないかというのが、さくらばの予想です。

言語仕様の変更がJEP 447, 456, 459, 463と4種類もあります。JEP 456だけがスタンダードJEPで使用しない変数やパターンを _ (アンダーバー)で省略して記述できるというものです。

スタンダードJEPであるJEP 423はG1GCのアルゴリズム改良、JEP 458はjavacでコンパイルすることなく複数のJavaコードを実行できるというものです。この機能は、JEP 330の拡張ですね。

あらたにPreview JEPになったのが、JEP 457とJEP 461です。

JEP 457はバイトコードを扱うためのAPIです。今までバイトコード操作というと、ASMなどが使われていましたが、標準のAPIで可能になります。

JEP 461はStream APIの拡張です。今まで中間操作はストリームの流れてくる1データに対する処理に限定されていましたが、Gathereを使用するとかなり柔軟に中間操作を記述することができるようになります。

 

と、軽くJEPを説明したところで、APIの変更について紹介していきましょう。JEPは多いのですが、意外にもAPIの変更は少ないです。ほとんどがJEP 454とJEP 457に関する変更です。ただし、今回もPreviewやIncubatorの変更は省略するので、JEP 457に関連したAPI変更はStandard JEPになった時に紹介します。また、前述したようにJEP 454 FFMは別エントリーで紹介する予定です。

例によって、セキュリティ関連のAPIは省略します。本バージョンでも、java.baseモジュール以外にもAPIの変更はありますが、使用頻度が低いAPIであるため、解説を省略します。

 

廃止になったAPI

Java 22では1つのメソッドが廃止になりました。しかし、もともと使用しても例外をスローする実装になっているので、廃止されても問題はないはずです。

 

メソッド

  • java.lang.Thread.countStackFrames()

スタックフレームをカウントするメソッドですが、Java 21まではUnsupportedOperationException例外をスローする実装になっています。

 

廃止予定のAPI

Java 22で追加された廃止予定のAPIはありません。

 

追加/変更されたAPI

Java 22のjava.baseモジュールで追加されたAPIは約300なのですが、そのうちの200以上がJEP 457 Class-File APIで、約30がJEP 454 FFM APIです。8割ぐらいは、この2つのJEP由来の変更ということになります。本エントリーでは残りの2割を紹介していきます。。

 

java.base/java.ioパッケージ

java.ioパッケージのConsoleクラスで1つだけメソッドが追加されました。

 

Consoleクラス

Consoleオブジェクトで扱っているデバイスがターミナルかどうかを調べるメソッドが追加されました。

  • static boolean isTerminal()

ターミナルというのは標準入出力に対応したデバイス(POSIXでいうところのtty)です。ターミナルであればtrueが返ります。逆にいうと、JShellやIDEのコンソールだとfalseになります。

jshell> System.console().isTerminal()
$1 ==> false

jshell>

 

java.base/java.langパッケージ

Java 22では、Unicode 15.1に対応したのでそれに応じたブロックの追加が行われました。これ以外にClassクラスとStackWalker.Option列挙型に追加があります。

 

Character.UnicodeBlockクラス

Unicode 15.1で追加されたブロックの定数が追加されました。

  • static final Character.UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_I

 

Classクラス

プリミティブ型に対応するClassオブジェクトを取得するメソッドが追加されました。

  • static Class<?> forPrimitiveName(String primitiveName)

引数にはプリミティブ型を表す文字列、たとえば"int"とか"double"を指定します。引数がnullの場合、NullPointerException例外がスローされます。

 

StackWalker.Option列挙型

StackWalkerクラスはスレッドごとに作成されるスタックフレームを操作するクラスです。Option列挙型はStackWalkerオブジェクト生成時に使用する列挙型ですが、定数が1つ追加されました。

  • StackWalker.Option DROP_METHOD_INFO

StackWalkerオブジェクトがスタックフレームを操作する時にメソッドの情報を扱わないように指定します。

 

java.base/java.lang.reflectパッケージ

いつものことですが、新しいリリースを表す定数が追加されています。

ClassFileFormatVersion列挙型

Java 22に対応する定数の追加です。

  • ClassFileFormatVersion RELEASE_22

 

java.base/java.netパッケージ

IPv4/IPv6のアドレスを表すInet4Addressクラス/Inet6Addressクラスは直接生成することはできず、スーパークラスのファクトリメソッドを使用していました。これに対し、それぞれのクラスにファクトリメソッドが追加されました。

 

InetAddressクラス

"127.0.0.1"などに対応するInetAddressオブジェクトを生成するにはセグメントの配列を使用するgetByAddressメソッドか、ホスト名も使用できるgetByNameメソッドを使用してきました。これに対し、アドレスを文字列で指定するファクトリメソッドが追加されました。

  • static InetAddress ofLiteral(String ipAddressLiteral)

ofLiteralメソッドでは、引数の文字列をまずIPv4と仮定してパースを行います。失敗した場合、IPv6としてパースします。パースに失敗するとIllegalArgumentException例外がスローされます。

実際の処理はInet4AddressクラスおよびInet6Addressクラスに委譲します。

 

Inet4Addressクラス

Inet4Addressクラスにもアドレスを文字列で指定するファクトリメソッドが追加されました。

  • static Inet4Address ofLiteral(String ipv4AddressLiteral)

アドレスの表記は今まで使用してきたのと同じです。d.d.d.d形式だけでなく、d.d.dからd.d、そしてd形式もパース可能です。

jshell> Inet4Address.ofLiteral("127.0.0.1")
$1 ==> /127.0.0.1

jshell> Inet4Address.ofLiteral("127.0.1")
$2 ==> /127.0.0.1

jshell> Inet4Address.ofLiteral("127.0.257")
$3 ==> /127.0.1.1

jshell>

 

Inet6Addressクラス

Inet6Addressクラスも同様にファクトリメソッドが追加されました。

  • static Inet6Address ofLiteral(String ipv6AddressLiteral)

アドレスの表記も従来と同じで、::や::d.d.d.d形式なども使用できます。

jshell> Inet6Address.ofLiteral("::1")
$1 ==> /0:0:0:0:0:0:0:1

jshell>

 

java.base/java.nio.charsetパッケージ

UTF-8などの標準的な文字セットを定数に持つStandardCharsetsクラスに定数が追加されました。

 

StandardCharsetsクラス

StandardCharsetsクラスではUTF-8やUTF-16系の定数は定義されていましたが、UTF-32系がなかったので追加されました。

  • static final Charset UTF_32
  • static final Charset UTF_32BE
  • static final Charset UTF_32LE

 

java.base/java.nio.fileパッケージ

Pathインタフェースにデフォルトメソッドが追加されました。

 

Pathインタフェース

Pathインタフェースにresolveメソッドのオーバーロードが2種類追加されました。いずれもデフォルトメソッドです。

  • default Path resolve(String first, String... more)
  • default Path resolve(Path first, Path... more)

実際の動作はfirstに対しresolveを行い、得られたPathオブジェクトに対しmoreを順々にresolveしていきます。

実際のコードは以下のようになっています。

    default Path resolve(Path first, Path... more) {
        Path result = resolve(first);
        for (Path p : more) {
            result = result.resolve(p);
        }
        return result;
    }

 

java.base/java.textパッケージ

リストをフォーマットするクラスが追加されました。

 

ListFormatクラス

ListFormatクラスはリストのフォーマッタークラスです。なぜになって導入されたのか、いまいち謎です。

他のフォーマッターと同様にスタイルなどを指定する列挙型も導入されています。

  • enum ListFormat.Style { FULL, SHORT, NARROW }
  • enum ListFormat.Type { STANDARD, OR, UNIT }

ListFormatクラスの使い方は他のフォーマッタークラスと同じです。getInstanceメソッドでListFormatオブジェクトを生成し、フォーマットするのであればformatメソッド、パースをするのであればparseメソッドを使用します。

主なメソッドを以下に示します。

  • static ListFormat getInstance()
  • static ListFormat getInstance(Locale locale, ListFormat.Type type, ListFormat.Style style)
  • String format(Object obj)
  • String format(List<String> input)
  • List<String> parse(String source)
  • Object parseObject(String source)

Objectクラスを引数にするformatメソッドと、parseObjectメソッドはFormatクラスで定義されたメソッドです。

また、引数のないgetInstanceメソッドはデフォルトロケール、STANDARD、FULLとなります。

StyleとTypeによるフォーマットの違いはListFormatクラスのJavadocにまとめられているので、参考にしてください。

個人的には日本語ロケールだと、ちょっと使いものにならない気が...

jshell> import java.text.*

jshell> var format = ListFormat.getInstance()
format ==> ListFormat [locale: "日本語 (日本)", start: "{0}、{1}", ... }", three: "{0}、{1}、{2}"]


jshell> format.format(List.of(0, 1, 2, 3))
$3 ==> "0、1、2、3"

jshell> format.format(List.of("a", "b", "c"))
$4 ==> "a、b、c"

jshell>

ここで示したようにデフォルトの日本語ロケールだと、リストの区切り文字に全角の"、"が使われます。それはちょっとなぁと思うわけです。

これに対し、たとえばUSロケールでSTANDARD/FULLだと次のようになります。

 jshell> var format = ListFormat.getInstance(Locale.US, ListFormat.Type.STANDARD, ListFormat.Style.FULL)
format ==> ListFormat [locale: "英語 (アメリカ合衆国)", start: "{0},  ... ree: "{0}, {1}, and {2}"]


jshell> format.format(List.of(0, 1, 2, 3))
$6 ==> "0, 1, 2, and 3"

jshell>

英語的には最後の要素が", and "となるのは分かるのですが、これを使いたいことがあるのでしょうか。

結局、よく使うのはTypeをSTANDARDではなくUNITにし、StyleはFULLかSHORTのような気がします。

 jshell> var format = ListFormat.getInstance(Locale.of("c"), ListFormat.Type.UNIT, ListFormat.Style.FULL)
format ==> ListFormat [locale: "c", start: "{0}, {1}", middl ... , three: "{0}, {1}, {2}"]

jshell> format.format(List.of(0, 1, 2, 3))
$8 ==> "0, 1, 2, 3"

jshell>

parseメソッドは戻り値の型がList<String>となることに注意してください。

 

java.base/java.util.concurrentパッケージ

Fork/Join Framework関連でメソッドが追加されました。いずれも割り込みに関するメソッドです。

 

ForkJoinPoolクラス

割り込みがかからないタスク実行のメソッドが追加されています。

  • <T> List<Future<T>> invokeAllUninterruptibly(Collection<? extends Callable<T>> tasks)

複数のタスクをまとめて実行する時に使用するのがinvokeAllメソッドですが、それに割り込みがかからないようにしたのがinvokeAllUninterruptibly()メソッドです。

このメソッドではタスクをjoinする時に、quitelyJoinメソッドを使用しているため、割り込みがかからないようになっています。

 

ForkJoinTaskクラス

ForkJoinPoolクラスとは逆に、割り込みがかかるタスクのファクトリーメソッドが追加されました。

  • static <T> ForkJoinTask<T> adaptInterruptible(Callable<? extends T> callable)
  • static <T> ForkJoinTask<T> adaptInterruptible(Runnable runnable, T result)

今までのadoptメソッドでタスクを生成した場合、タスクに割り込みをかけることができませんでした。これに対し、adaptInterruptibleメソッドではタスクに対して割り込みを書けることができます。

戻り値の型はForkJoinTaskクラスですが、実際には派生クラスのInterruptibleTaskクラスのさらに派生クラスであるAdaptedInterruptibleCallableクラスが戻ります。

2種類のオーバーロードの違いは、引数の型が違うのでjoinした時の戻り値に違いがでるということです。Runnableインタフェースではタスクの戻り値がないので、adaptInterruptibleメソッドの第2引数のresultが返ります。

 

java.base/java.util.randomパッケージ

乱数値のストリームを生成するメソッドが追加されています。

 

RandomGeneratorクラス

RandomGeneratorクラスではdoubleの乱数値のストリームを生成するdoublesメソッドがあります。これの派生メソッドが追加されました。

  • default DoubleStream equiDoubles(double left, double right, boolean isLeftIncluded, boolean isRightIncluded)

doublesメソッドでは要素数を指定しますが、equiDoublesメソッドは無限ストリームになります。

引数は境界値で、その境界値を含むかどうかを第3, 4引数で指定します。

 

 

Java 22のAPI変更について紹介しましたが、やはり少ないですね。

また、これは便利だとか、使えそうというAPIの追加もないようです。

とはいうものの、FFMは外部ライブラリを使いたい人には有用ですし、Class-File APIもASMを使っていた人にはうれしいはず。といっても、これらを使う開発者はごくごくわずかだとは思います。

普通の開発者であれば、ストリームのGathererは便利に使えるはずです。GathererがStandard JEPになるまで、待ちましょう!

 

さて、次のJava 23では、長らくIncubatorだったVector APIが入るかどうかです。最近はVector APIのAPI変更もないようなのですが、一波乱あるのかどうか。ぜひ入ってほしいなぁ。

2024/03/17

Jfokus 2024 その2 セッション編

このエントリーをはてなブックマークに追加

前回に引き続き、Jfokusの参加記です。

www.javainthebox.com

 前回はJfokusに参加するまでの話ですが、本エントリーではさくらばがJfokusに参加して興味深かったセッションを紹介します。


Jfokusは3日間の会期中、1日目がチュートリアルとハンズオンが行われます。2, 3日目が通常のセッションです。


Java 21 Deep Dive - Better Language, Better Scalability, Better APIs, Better Tools

チュートリアルで聴講したのがこれ。おなじみのOracleのアドボケイトのNicolai ParlogとAna-Maria Mihalceanuのセッションです。

資料はこちら。

slides.nipafx.dev


Pattern Matching、Virtual Threads、String Templatesが前半で、後半はSequenced Collectinosなど細かな機能、最後にツール系を紹介していました。

まぁ、さくらばには、ほとんどが知っていることだったので、機能の再確認をしたという感じです。

適度にまとまっているので、機能のチェックをしたいのであれば、ちょうどいいと思います。


Java in 2024

Jfokusのキーノートセッションで、こちらもおなじみ、OracleのGeorges Saabです。


当初は、JavaのチーフアーキテクトのMark Reinholdが話す予定でした。しかし、Markの来訪がキャンセルになってしまって、急遽Georgesになりました。

さくらばはMarkのセッションを楽しみにしていたので、キャンセルと聞いてモチベーションダダ下がり。さらにVM Tech Summitもなくて、さらにモチベーションが下がる。

そのモチベーションの低さが会場の写真などをほとんど撮らなかったことにつながるわけです。

Georgesの話はいつも通りな感じですね。


Java Language Update

Devoxx BEでBrian Goetzが話したセッションのアップデート版。JfokusではOracleのViktor Klangが担当。


最近、ViktorさんとParさんが話すことが多いようなんですけど、そういう役割なんですかね。

Java 21だけでなく、Java 17ぐらいからのJava言語仕様の変化についてまとめたセッションです。


Enter the Parallel Universe of the Vector API

AzuleのSimon RitterのVector APIに関するセッション。


資料はこちら。

Enter The Parallel Universe of the Vector API


Vector APIについての分かりやすい解説。これを見ておけば、だいたい理解できるんじゃないかなぁ。資料だけだとちょっとつらいかもしれないですが。

Java 22でFFMが正式に導入されるので、Vector APIももうすぐですね。


Modern Java in Action

こちらもNicolai Parlogのセッション。


資料はこちら。

slides.nipafx.dev


GitHubをクロールしてするサンプルアプリケーションを古いスタイルから新しいスタイルに書き換えていくというライブコーディングのセッション。

なかなかおもしろいけど、早すぎて途中からついていけないのが...


これ以外にも、Ubertoの関数型のセッションや、ParさんのLeydenのセッション、Datadogのプロファイラー、AlinaとShaunのGraalVMなどを聴講しました。

それにしても、VM Tech Summitがなかったのがイタイ。

来年は、VM Tech Summitがあれば参加するつもりですが、ないのならばやめようかなぁと思うさくらばなのでした。

2024/03/16

Jfokus 2024 その1 準備編

このエントリーをはてなブックマークに追加

2月5から6日にかけて、スウェーデンのストックホルムでJavaのカンファレンスのJfokus 204が開催されました。今回、日本人の参加者は私を含めて3人しかいませんでした。

ぜひ、来年は日本からの参加者が増えるといいなぁということで、Jfokusの備忘録です。

なお、2月3, 4日にはベルギーのブリュッセルでFOSDEMというカンファレンスも開催されています。FOSDEMとJfokusの両方とも参加される方も多いのですが、さくらばはJfokusだけ参加しました。

今回はあまり写真を撮っていないので、会場などの写真はほぼないです。すみません。


Jfokus

Jfokusはスウェーデンのストックホルムで2007年から開催されているJavaのカンファレンスです。VM Tech SummitというVMに特化したイベントも一緒に行っているなど、ちょっとデープなカンファレンスになっています。

しかし、コロナ後はVM Tech Summitが開催されておらず、普通の大規模なJavaカンファレンスになっていました。今年は、事前にはVM Tech Summitもあると言われていたのですが、結局なかったらしいです。かなり残念。

www.jfokus.se

参加者は1,000人ぐらい?上述したようにFOSDEMから参加している方も多いようです。

スウェーデンでの開催ですが、セッションは英語です。スウェーデンはTOEICの国別ランキングで常に上位にいる国なので、どこでも英語でOKのようです。


チケット

JfokusはDevoxxのようにチケットの争奪戦になることはないですが、売り切れることもあるので早めに取得するのがいいと思います。また、Devoxxとは異なり、クレジットカードに対応しています。


ストックホルムへ

Jfokusに参加することが決まったら、まずやることは交通手段と宿泊の確保です。


空路

ストックホルムはアーランダ空港とスカブスタ空港がありますが、ほとんどがアーランダ空港になると思います。ただし、ライアンエアーなどの航空会社はスカブスタ空港をしていますが、まぁ使うことはないと思います。

現状、アーランダ空港への直行便はないので、どこかしらで乗り継ぎを行う必要があります。

スウェーデンはシェンゲン協定加盟国なので、フランクフルトやパリから乗り継ぐ場合は乗り継ぐ場所で入国審査を受ける必要があります。このため、乗り継ぎには余裕をみてスケジュールしたほうがいいです。

イギリスなどシェンゲン協定に加盟していない国はスウェーデンで入国審査を受けます。

今回、さくらばはロンドン ヒースロー空港で乗り継ぎで、アーランダ空港着でした。


ストックホルム市内へ

アーランダ空港からストックホルム市内へは、鉄道のアーランダエクスプレスを使うのが便利です。

ターミナル5に直結したアーランダ北駅と、ターミナル2, 3, 4から利用できるアーランダ南駅とストックホルム中央駅を結ぶ鉄道で、だいたい40分ぐらいでストックホルム中央駅に着きます。

チケットは空港でも購入できますが、事前にオンラインで購入する方がいいと思います。オンラインで購入すると、QRコードが発行されるので、それを駅でスキャンすればOKです。

列車内で検察にくることもあります。

www.arlandaexpress.com


宿泊

Jfokusの会場はストックホルム中央駅に直結したStockholm Waterfront Congress Centreという場所で開催されます。

なので、中央駅近辺でホテルを探すのがいいと思います。

一番いいのは会場と同じ建物にあるRadisson Blu Waterfront Hotelですが、周りのホテルに比べると宿泊費は高めです。なお、すぐそばに同じ系列のRadisson Blue Royal Vikingというホテルもあるので、お間違えなく。

Radisson Blue Waterfront Hotelは新しい建物なのですが、中央駅の近辺は古い建物が多いのでホテルも古いところが多い感じです。ちょっと離れると新しいところもあるので、会場まで近いという利便性をとるか、施設のよさをとるかのどちらかですね。

今回、さくらばはFreys Hotelに宿泊しましたが、ここもかなり古い建物でした。


気候

北欧と聞いたらやっぱり寒いと思いますよね。

今年のJfokus会期中は一番寒い日で最低気温-10度、最高気温-3度ぐらいでした。

Jfokusの前の週がかなり暖かくて、前々週は最低気温が-20度になるぐらいの寒さだったようです。

したがって、行くとしたら、最低気温が-20度になっても耐えられるぐらいの服装で!今年もそこまで寒いというわけではなかったですが、日本からの参加者の1人が寒さで風邪をひいてしまっていました。せっかくカンファレンスに参加するのですから、体調を崩さないように、万全の体制で挑むようにしましょう。

前週が暖かったということから、今年は街中はほとんど雪は残ってませんでした。昨年は雪も残っていて、道がアイスバーン化しているところもあったらしいので、靴もそれ用に用意したほうがいいと思います。

ストックホルムは湖に面した街ですが、最低気温が-10度にもなると湖も運河もカチカチに凍りますね。


Jfokus会場

Jfokusの会場は前述したようにWaterfront Congress Centreです。ストックホルム中央駅からほぼ直結してます。

ただ駅の裏側なので、ちょっと分かりにくいかもしれません。

古い建物が並ぶストックホルムの中心街の中で、ここだけは妙にモダンな建物になっています。

運河に面している建物ですが、運河の向かい側は市庁舎です。私は知らなかったのですが、魔女の宅急便に出てくる時計台の尖塔は、この市庁舎がモデルらしいです。

入り口にはクロークがあるので、上着はここで預けられます。会場は暖かく、上着は邪魔になるだけなので、預けた方がいいと思います。


食事

Jfokusは、朝食とランチが提供されます。

朝食にはスウェーデン名物でもあるカネルブッレ(シナモンロール)やカルダモンマブッレ(カルダモンロール)、オープンサンドのスモーブローなどが出されます。

昼は日によって異なりますが、スウェーデン料理のプレート。けっこうおいしいです。

また、会期2日目の夜は展示会場でレセプションがあり、軽食がでます。ただ、おなかがいっぱいになるほどではないですね。

気をつけなくてはいけないのが、レストランが意外と早い時間にしまってしまうこと。行くところが決まっているのであれば、予約してからいくのがいいようです。

逆に早朝からやっているカフェやイートイン併設のパン屋さんは多いんですけどね。


後編では、さくらばがJfokusで聴講したセッションの中から面白かったものについて紹介します。

2024/02/02

なぜあなたはラムダ式が苦手と感じるのか

このエントリーをはてなブックマークに追加

今年もブリ会議で講演してきました。例年、Javaの新しい機能などについて話すことが多かったのですが、今年はProject Lambdaから10年ということもあり、あらためてラムダ式についての話です。

2部構成で前半が初心者向けの「なぜあなたはラムダ式を苦手と感じるのか」というラムダ式を使う立場での話。後半はぐっと難易度があがって、「ラムダ式はどうやって動くのか」という内容です。

このエントリーはブリ会議で話した内容の前半部分の解説です。資料はこちら。


ラムダ式が導入されたのは2014年、Java 8の時です。今年でちょうど10年ですね。

10年もたったのですから、ラムダ式を当たり前のように使っている開発者も多いとは思いますが、いまだに苦手と感じている方がいらっしゃるのも事実だと思います。

ラムダ式が導入後にJavaを使い始めた方でも苦手という方がいるんですよね。

ラムダ式は、Javaで関数型プログラミングの考え方が導入された端緒です。ラムダ式と一緒に導入されたStream APIをはじめ、今では関数型プログラミングに関する機能がJavaではどんどん増えています。

たとえば、言語仕様では

  • switch式
  • Record
  • Sealed Class
  • パターンマッチング

など。RecordとSealed Classは、両方を組み合わせて代数的データ型 (Algebraic Data Type, ADT) を構成するのに使われます。

標準のAPIだと、次のようなAPIがあります。

  • Stream API
  • Flow (Reactive Stream)
  • HTTP Client

一番使われているのはもちろんStream APIですね。HTTP Clientは、Java 11で導入されましたが、Flowを使用して宣言的にHTTPのクライアントを記述します。

標準API以外でも、Spring WebFluxや、Oracle Helidon、Red HataのRed Hat Quarkusなど関数型プログラミングの考えを使うライブラリやフレームワークも増えています。

つまり、これからはJavaを使っていても関数型プログラミングからは逃れられなくなっているのです。

関数型プログラミングの考えを取り入れたプログラミングスタイル、つまり今までの手続き的な記述から宣言的な記述に変えていかなくてはなりません。

さらにいうと、今までのJavaはどうしても文で考えがちなのですが、そうではなく式を使って記述することを意識していきたいのです。


ラムダ式とは

さて、そのラムダ式ですが、端的にいえば関数です。名前はないので、無名関数ということができます。

無名関数といっても、メソッドと同じようなものです。Java Language Specificationのラムダ式の説明の一番はじめには、次のような記述があります。

A lambda expression is like a method: it provides a list of formal parameters and a body - an expression or block - expressed in terms of those parameters. (JLS 15.27)

つまり、メソッドが書ければラムダ式は書けるはずなのです。

もちろん、ラムダ式は通常のメソッドとは異なり、クラスに属していなかったり、ラムダ式の外側のクラスの状態にアクセスすることも制限されていたりするので、メソッドと同じというわけではありません。

しかし、それは今までの手続き的な記述にとらわれてしまっているからではないでしょうか。


手続き的な記述の欠点

手続き的な記述には読みにくいポイントや、バグを発生しやすくしてしまうポイントがあります。

代表的なポイントを以下に示しました。


1点目の複数の処理を一緒に書けてしまうというのは、処理を十分に分解せずにまとめて書きがちだということです。

残りの2点は変数に関することです。手続き的な記述だと、どうしてもy処理の途中経過を保持するなどの変数が使われます。途中経過を保持させるので、ミュータブルになってしまうわけです。

また、途中経過を保持させて後に、最終的な結果を得るためには別のスコープに入ることがあり、スコープが広くなってしまいがちです。

言葉で書いても分かりにくいと思うので、具体的なコードで見てみましょう。

たとえば、A組の生徒の平均値を算出することを考えてみます。

生徒の成績は次のRecordで保持しているとします(通常のクラスでもいいのですが、こういうデータを保持させるにはイミュータブルなRecordの方が適しています)。

    record StudentScore(
            String name,
            String className,
            int score) {}

StudentScoreでは生徒の名前と、クラス、成績を保持させています。

平均値を求めるメソッドを次に示します。

    double calcAverage(List<StudentScore> scores) {
        double sum = 0.0;
        int count = 0;
 
        for (int i = 0; i < scores.size(); i++) {
            var ss = scores.get(i);
            if (ss.className().equals("A")) {
                sum += ss.score();
                count++;
            }
        }

        sum /= count;

        return sum;
    }

一見、よさげに見えますが、何が問題なのでしょう。

まず、ローカル変数のsumとcountです。この2つの変数はいずれもループでの中間値を保持させるためにミュータブルになっています。

また、ループが終わった後の最終的な結果を求めるために、ループの外側でも変数を使用します。このため、変数のスコープがメソッド全体になってしまっています。

ミュータブルだと意図しない値の変更がされる可能性があります。このメソッドは行数が少ないからよいですが、行数が多いメソッドを複数人で編集していたりすると意図しない変更が起こりがちです。

スコープが広い変数だとなおさらです。

たとえば、このメソッドでは最後にsumをcountで割って平均値を求めていますが、変数sumはループの中では合計値を保持させています。合計値を保持させる変数であるのに、最後に合計値ではない値を代入しているわけです。

平均値を代入した後に、他の人が合計値だと思って変数sumを使ってしまうとバグが発生してしまいます。


そして、ループです。

ループというか、繰り返し処理ってやっぱり分かりにくいと思うんですよ。

慣れてしまえばパターンとして覚えてしまうので、パッと書けるとは思いますが、初心者には難しい。

ここでのたった7行のループで、ループの制御、値の取り出し、比較、合計処理、カウンターのインクリメントまでやっています。特にループカウンターを使ったループの制御は本来の平均を求める処理とは別個のものなので、一緒にしてしまうと分かりにくくなってしまいます。

ここでは普通のfor文で書きましたが、for-each (拡張for文)で書いたとしても本質的な難しさは変わりません。

ループの制御はコンテナ側に任せ、ループで扱うデータに対し何を行っていくのかを分解して考えることで分かりやすさ、読みやすさは格段に向上します。

これが宣言的に記述するということにつながります。

for文という文ではなく、式で処理を連ねていくわけです。


宣言的な記述

先ほどの平均値を求めるメソッドを宣言的に記述したのが以下のコードです。

    double calcAverage(List<StudentScore> scores) {
        final var ave = scores.stream()
                              .filter(ss -> ss.className().equals("A"))
                              .collect(Collectors.averagingDouble(ss -> ss.score()));
        
        return ave;
    }

この記述には、return以外は文が使われておらず、式で処理を記述しています。

そして、ローカル変数のaveはイミュータブルな変数になります。

Stream APIを使ったループはいわゆる内部イテレータになり、ループの制御はStream APIが行います。Stream APIを使う側は、データをどのように処理するかだけに集中し、それをラムダ式で記述します。

行っている処理自体は手続き的に記述しても、宣言的に記述しても同じです。

しかし、Stream APIを使うことで、条件、値の取り出しなどの処理をそれぞれ1つのラムダ式で記述することで、処理の流れが分かりやすくなります。

とはいっても、今まで手続き的な記述しかしてこなかった方には、宣言的な記述はとっつきにくい感じを受けてしまうのはしかたないと思います。

だからといって、今までの手続き的な記述に固執していたら、どんどん増えている宣言的スタイルのライブラリやフレームワークを使えなくなってしまいます。

いつかは宣言的な考え方をしなくてはいけないのであれば、今がそのチャンスです。


重要なのはシグネチャー

前述したように、ラムダ式は関数として扱うのが自然です。

しかし、関数型インタフェースがとかを考え始めてしまうと、なかなかとっつきにくくなります。

Javaにも関数型があればこんなことに悩む必要はないのですが、残念ながらJavaでは関数型はありません。しかたないので、関数型インタフェースなんてものを持ち出したわけです。

しかし、ラムダ式を関数と考えるのであれば、型はそれほど重要ではありません。

重要なのはシグネチャーです。つまり

  • 引数の個数と、その型
  • 戻り値の有無と、その型

が重要になります。引数や戻り値の型はジェネリクスの型パラメータで定義されるので、引数の個数や戻り値の有無から考えましょうということです。

たとえば、数値を保持しているリストをソートするには以下のように記述します(もっと簡単に書けますけど、説明のためこう書いてます)。

    List<Integer> nums = ...;

    var sortedNums = nums.stream()
                         .sorted((x1, x2) -> x1 - x2)
                         .toList();

sortedメソッドの引数のラムダ式で要素同士を比較して、ソートの並び順を決めています。

このラムダ式はComparator<S>インタフェースなのですが、実際にはBiFunction<S, S,  Integer>インタフェースだとしても、処理はまったく同じです。

つまり、sortedメソッドの引数にするラムダ式では2つの引数が渡されて、戻り値としてintで戻すということが重要になるわけです。


java.util.functionパッケージのインタフェース

ラムダ式は関数で、重要なのはシグネチャということですが、ではFunctionインタフェースなどのjava.util.functionパッケージで提供されている関数型インタフェースはどのように考えればよいのでしょう。

インタフェース自体の用途などは気にせずに、シグネチャーを区別するためのものと割り切ると理解しやすいです。

つまり、シグネチャーが

  • 引数なし、戻り値ありならば Supplier<T>
  • 引数が1つ、戻り値ありならば Function<T, R>
  • 引数が1つ、戻り値がbooleanならば Predicate<T>
  • 引数が1つ、戻り値はなしならば Consumer<T>

のように考えるわけです。

たとえば、Streamインタフェースのmapメソッドの引数はFunctionインタフェースですが、その場合は引数が1つで、戻り値ありのラムダ式を書けばいいのだなと分かるわけです。


ちなみに、ラムダ式を関数型インタフェースの匿名クラスの延長として考えてしまうのは危険です。それだと、いつまでたっても手続き的な考え方に執着してしまいます(実際に動作としても匿名クラスとラムダ式はまったく異なるのですが、それはブリ会議の後半のエントリーで説明します)。

そんな変なことを考えずに、ラムダ式は関数として考えましょう。そして、ラムダ式を書く時にはシグネチャーから処理を記述していきましょう。


ラムダ式を書く時のTips

ここまでラムダ式は関数として扱いましょうということを説明してきたわけですが、では実際にラムダ式を書く時にどうすればよいでしょう。

ラムダ式は単独で使うということはほぼなく、ほとんどがライブラリやフレームワークのメソッドの引数として使います。

ということはラムダ式を単体で考えるのではなく、ライブラリやフレームワークと合わせて一緒に考えればよいということです。

ちなみに、メソッドの引数や戻り値に関数を使用することを高階関数と呼びます。名前はどうでもいいのですが、メソッドの引数にラムダ式というのがラムダ式の主流の使い方ということです。

Project LambdaのスペックリードのBrian GoetzはJava Magazineのインタビューで次のように語っています。

Project Lambdaは単なる言語機能ではなく、ライブラリも対象としています。言語機能とライブラリが一体となって、Javaプログラミング・モデルを大幅にアップグレードします。 (Project Lambdaの展望, Java Magazine Oct. 2012)

ラムダ式を策定したProject Lambdaではラムダ式とStream APIを合わせて策定しています。

このようにラムダ式単独ではなく、ライブラリやフレームワークと一緒に使うことを前提にラムダ式を考えていきましょう。


次の処理は1つだけというのは、ラムダ式はなるべくシンプルにしましょうということです。ラムダ式のボディにいろいろ処理を書いてしまうというのは、処理を正しく分解できていないということにつながり、どうしてもラムダ式のボディが手続き的になってしまいます。

処理を分解して、1つのラムダ式には単純な処理を記述し、それを連ねていくというスタイルに変えていきましょう。


3つめはラムダ式で扱うデータをなるべく引数だけにするということです。

ラムダ式は、ラムダ式が定義されている外側のクラスのフィールドなどにもアクセスできますが、それは可読性の低下につながります。もし、外部のデータにアクセスするのであれば、定数などに限定しましょう。


4つめの処理結果を戻り値以外で戻さないというのは、3つめの外部のデータにアクセスしないにも通じます。関数なのですから、結果は戻り値だけです。


最後はラムダ式だけでなく、宣言的な記述全般にいえることですが、変数はイミュータブルにしましょう。


まとめ

さて、前半のまとめです。

なんども書いていますが、ラムダ式は関数としてあつかい、宣言的な記述を行うために役立てるのがお勧めです。

匿名クラスの延長として考えてしまうと、今までの手続き的な考えの枠内にとどまってしまい、宣言的な記述を書くことの妨げになってしまいます。

Javaは今までの手続き的な記述から、宣言的な記述にプログラミングスタイルが変わってきています。今後もこの流れは変わりません。

ですから、手続き的な考えでラムダ式を理解しようとすることはやめましょう。

宣言的な記述、文から式への移行を役立てるためにラムダ式が存在するのです。


最後に参考文献にあげた「なっとく! 関数型プログラミング」を紹介しておきます。

この本はJavaからScalaへの移行を解説した書籍です。

しかし、手続き的なJavaから、宣言的なJavaへ移行するにも役立つはずです。

トピックはだいたい1ページで収まっており、読みやすいのもいいです。

ただ、Kindleだと画像の解像度が低くて、ちょっと読みにくいのが欠点です。翔泳社さん、どうにかしてくれないですかねぇ。

www.amazon.co.jp


さて、ブリ会議で後半に解説したラムダ式がどのように動作するのかについては、次エントリーで紹介する予定です。