本セッションの登壇者
セッション動画
LINE Fukuokaのきしだ(Twitter: @kis)です。
Java 20とその先も含めてオブジェクト指向からデータ指向プログラミングへという話をしたいと思います。
実際の用途に合わせて処理を軽く
Java 20リリース記念なのでまずはJava 20の話をしましょう。
Java 20の今回のJEPとは、Javaに今から搭載する新機能のまとまりのことで、JDK Enhancement Proposal の略です。全部が試用機能(プレビュー)で、新たなJEPは1つだけです。
大きく3つに分けて、並列実行関連が3つ、パターンマッチング関連が2つ、低レイヤーが2つです。低レイヤーのForeign Function & Memory APIやVector APIはいつまでたってもStandardにならないので、しばらく正座して待っておく感じでよさそうです。
新しく入ったScoped Valueは、メソッド間でのスレッドセーフ共有を手軽に行うものです。左のソースのようにstr
という変数に「test」と入れてproc1、proc2で使う場合、Webではマルチスレッドで各ページが処理されるので、String str
を使うと、同時に使ってる人の情報が間違って出てきたりするのでThreadLocal
を使う必要があります。しかし、ThreadLocal
は高機能で重たい上、引数でproc1、proc2に渡していくのも面倒くさい。ちょっとしたメソッド間の値の共有などのためにThreadLocal
を軽くしたものがScopedValue
です。ここではSTRという変数を使いましたが、後で説明するString TemplateでSTRが使われるので、別の変数にしたほうが良いでしょう。
次に、Virtual Threadです。OS管理のスレッドの処理もまたとても高機能だったので、複数リクエストの同時処理などのためにはOS管理ではなくJavaが管理するスレッドが入りました。
Structured Concurrencyは、同時に複数のスレッドが走るときにタイミングを取るための仕組みです。オブジェクトクラスのjoinやwaitは処理が重すぎるのと、waitしているところによそでjoinをしたらwaitが解放されて次に続くという処理が3次元のgo toみたいになって、管理が大変なので導入されました。このように、今回の並列実行関連は、今まである仕組みが実際のシチュエーションには高機能過ぎるので軽いものを作るという施策です。
ただ、だいたいはフレームワークがラップしてくれるので、意識して使うことはあまりないと思います。
あと、小さい変更が2つあって、こちらのほうが重要です。第1に、URLクラスのコンストラクタは非推奨にして、URIを使用すること。第2に、16ビット浮動小数点数に対応したことです。最近流行っているAIの機械学習時には、高精度が必要なので、たとえばChatGPTにいろいろな文章を読み込ませて学習させるときは64ビットの浮動小数点数を使用します。
しかし、ユーザーとチャットするときは精度を落として32ビットか16ビットで十分です。Javaでは16ビット浮動小数点数が扱えなかったので、これに対応しました。ただ、実際にはCPUがそもそも16ビットの浮動小数点数の計算命令を持っていないので、GPUやAI用のプロセッサにデータを渡すときに使います。この変更が必要になる背景がちょっとおもしろいですね。
Javaで小さいプログラムも書きやすく
Java 20の話はここまでで、Java 21ではJavaを使いやすくするための変更が加わります。Javaは大規模な業務用システムを動かすのが目的だったので、オブジェクト指向になっていたり、文法も省略せず長く書かれていました。また、ちょっとしたコードを書いたり「Hello world!」を出力するのも手間がかかっていましたが、Java 21では小さいプログラム作りにも目を向けてきています。
Java 11から、ソースコード1つで書ける単一ソースコードファイルのプログラムが、javacせずに直接javaコマンドで実行できるようになっています。これを複数ソースコードに拡張するJEPができていて、Java 21か22に入りそうです。Javaはコンパイルしないと動かせないというのは正しくなくなってきていますね。
mainを最低限の記述でというのがJEP445です。Javaで最初にクラスを定義する際にはいろいろな「おまじない」が必要でしたが、右下の例のように、クラスを書かなかった場合は匿名クラスとなり、クラス定義が不要になります。JEP445の段階ではこの右下のコードを書くだけで最初の「Hello world!」ができるようになります。Java 21は長期サポート(LTS:Long Term Support)版なので結構大事なバージョンなんですね。プレビュー機能であっても、そこにこのような改善が入るのはとてもいいことです。
文字列への式の埋め込みも、プレビューとしてJava 21に入ります。今まで文字列に数値を埋め込む場合は「+」で連結したりprintfやString.format / formattedを使っていましたが、直接式を埋め込めるString Templateが導入されます。ここでSTRが出てきて、STR."" のダブルクォーテーションの間に式が書けます。このString Templateは拡張可能なので、いろいろなカスタマイズが可能です。これはもうすぐ入ると思います。
「レコード」クラスの導入でデータ指向プログラミングに舵を切る
ここからはオブジェクト指向からデータ指向プログラミングへという話をします。オブジェクト指向はプログラムをモジュール化する技法ですが、すでにWebのサーバー側とクライアント側はモジュールで完全に分かれています。サーバー側でもマイクロサービス化でプログラム自体が最小限のモジュールになっているので、それをさらに分割する必要がなく、オブジェクト指向がいらなくなっているという現状です。その中で、データをデータとして扱うという考え方が出てきました。それがデータ指向プログラミングです。
まず、クラスにはモジュールの性質と型の性質がありますが、実際にはどちらか片方が主になります。その観点で分類すると、クラスは大きくシステム境界、データ、処理の3つになります。
抽象データ型と代数的データ型という考え方があります。抽象データ型は1つの型をどういうふうに定義するかという指針のこと、代数的データ型は型を組み合わせるときの指針です。1つのクラスは抽象データ型で書いて、クラスを組み合わせるときに代数的データ型で書きます。これは新しい考え方ではなく、1970年くらいからある考え方に名前が付いていると考えてください。
抽象データ型とは、データの操作だけ公開することで変更に強く柔軟な型を定義しようという考え方です。途中で実装を変えても動作が変わらないように注意深く実装されていれば問題ありません。カプセル化との違いは、カプセル化という考え方はオブジェクト指向だけではなく単にモジュールを作るときにも適用でき、隠せるものは隠して必要なものだけをオープンにしますが、操作には言及しません。抽象データ型はデータに特化して、どういう操作を持っておくかまでを含みます。オフィシャルな定義ではありませんが、そのように理解すると良いと思います。
今回、抽象データ型を実現する仕組みとしてレコードが導入されました。レコードは他の型を継承することもできないし、継承して拡張することもできません。ただしインタフェースの実装は可能になっています。
データ型のルールはあまりきちんと定義されているものがないのですが、メソッドを定義するときの考え方の純粋関数(pure function)をデータ型に拡張して、純粋データ型と言うといいのではないかと思っています。参照透過とは、同じ引数を与えたら戻り値が同じになることで、副作用なしとは状態の変更や外部への入出力がないことです。これをデータ型に拡張すると、参照透過でデータ型が持っているメソッドの戻り値が引数とフィールドだけで決まり、副作用なしでフィールドの変更以外の状態変更や外部への入出力がないということですね。だいたいはもうそうなっていると思います。
今度は代数的データ型の話です。代数というのは四則演算ですが、引き算も足し算、割り算も掛け算なので、掛け算と足し算を考えておけば良いということで、直積型と直和型という分類があります。直積型になるものとしては、クラス、配列、レコードクラスで、直和型になるものは継承、sealedです。sealedも継承に制約を付けるだけなので、基本的にはJavaでは直和型は継承を使います。
直積型というのは、値を組み合わせる型です。代表例はレコードで、record A がbooleanとbyte型を持つと定義すると、booleanはtrue/false、byte型が-128から127なので、このレコードは2×256=512とおりの値を持ちます。持っている型の組み合わせの掛け算になるのが直積型です。
直和型は足し算で、いずれかの型になります。catchでのみ、IOExceptionかinterruptedExceptionを取るというマルチキャッチができます。他の、たとえばjava.nio.BufferだとIntBufferとByteBufferがバッファを継承して、どちらかになります。
バッファの場合は、コンストラクタをパッケージプライベートにすると他のパッケージから操作不可にできます。java.nio.Bufferの場合、javaパッケージは新たに追加されることはないけれど、アプリケーションクラスでは他のユーザーがJARファイルを後から追加できるので、可能な型を限定できません。そこで、sealedという型が導入されています。ここではTypeというインタフェースをsealed型にして、そこにBulkとPackedが入るようにしています。
データ指向プログラミングで処理を主体に書くべき
BulkとPackedというTypeを持ったProductというレコードで、商品の価格を計算する例を考えます。
Streamで書いたらこうなります。instanceofでキャストをしてというのが面倒くさかったわけですね。
それが、パターンマッチを導入してinstanceofの型の後に変数を定義できるようになりました。で、ちょっとダサくなりました。
今回プレビューになっているレコードパターンというものが、Java 21でスタンダードになります。そうするとBulkのpriceとunitを、パターンマッチのときに分解できるようになります。
また、Switch式でパターンマッチが使えるようになるので、右下のようにすっきり書けるようになります。sealed型を使ったのでdefaultがいらなくなっています。
Switch式でこのようなコードはJava 20で動かなかったんですが、4月にリリースされたJava 20.0.1では修正されています。みなさん、最新版を使いましょう。
BulkとPackedのところを継承で実装したらいいんじゃないかということで、軽くメソッドを持たせると、右下のように計算処理ができます。オブジェクト指向ですっきりしたように見えますが、結局右下のコードを追うときは左側のコードを全部追わないといけないので、書いた人だけすっきりした気持ちになる例です。
実際、仕様変更に弱くて、他のデータが関係すると対応できないんですね。この例では、1パック170円で3パックで500円などのように商品種別や時間帯が変化すると、結局は場合分けが必要になってきます。このように、オブジェクト指向できれいに書いたとしても、業務アプリケーションだとすぐに崩れるのが「オブジェクト指向プログラムが見にくくなる」正体です。業務処理だと1か所にすべての型の処理をまとめられるようにデータ指向プログラミングで処理を主体に書くべきです。ただ、システム境界になるクラスだと、他の型が関係することがないので継承が有効です。たとえば、OracleConnectionの処理でMySQLConnectionの状態は気にしなくて良いですよね。
今言ったような話を、発売中の『WEB+DB PRESS』vol. 134に僕が書いています。また、4月に『データ指向プログラミング』という本が出ていて、今まで言ったような「オブジェクト指向いらんやん」みたいな話が整理されて書かれていると思うので読んでみると良いと思います。
まとめます。Java 21への期待と、パターンマッチやレコードなどはデータ指向プログラミングのために入っていると考えると良いという話でした。
ご清聴ありがとうございました。