Pattern Matching for instanceof - instanceofを使ったif文がより簡素に記述可能
instanceofを使ったインターフェースの実装クラスごとに処理を分ける場合、これまでであれば、リスト9のようなコードを記述していました。
// これまでの記述方法
if( obj instanceof Sample){
((Sample) obj).exec(); // (1) Sampleクラスへのキャスト
}
instanceofで判定した後、指定したクラスにキャストしてから、その型固有の操作を行うわけです((1))。しかし、Java 15からは、リスト10のような記述が可能になりました。
//(1) キャストも同時に行う記述
if( obj instanceof Sample sample){
sample.exec();
}
// (2)キャスト後のメソッドもif文の中で利用可能
if( obj instanceof Sample sample && sample.isVisible()){
sample.exec();
}
(1)のように、同時にキャスト済みのインスタンス変数(ここではsample)を定義できます。また、(2)のようにそのインスタンスメソッド(ここではisVisible)を条件式の中で利用することも可能です。
Records - 参照用データ管理のクラス定義が簡単
Javaでは、これまでデータの固まりを扱う際にも、通常のクラスとして定義する必要がありました(例えば、C言語での構造体を知っている方であれば、イメージがつきやすいと思います)。
このRecordsという機能はまさに、特別なメソッドを持たないデータへのアクセスのみができる構造体を管理するような用途を意図したものです。例えば、よくあるケースで座標(x,y)などの値を返す場合に、リスト11のようなクラスを作成する必要がありました。
public class Point {
private final int x,y;
public Point(final int x, final int y){
this.x = x;
this.y = y;
}
// 値を取得するためのメソッド
public int x(){
return this.x;
}
public int y(){
return this.y;
}
// Objectクラスでのメソッド
public boolean equals(Object o) {
//(省略)
}
public int hashCode() {
return Objects.hash(x, y);
}
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
上と同じ意図のコードを、Recordクラスを用いて書き換えたものがリスト12です。
// (1) recordクラスの定義
public record Point(int x, int y){}
// (2) 利用する場合
Point point = new Point(100,100);
Recordクラスは、「class」ではなく「record」を使って定義します((1))。Recordクラスでは、x()、y()とequals()、hashCode()、toString()などは自動生成されるのですべて省略できます。ただし、途中で値の変更はできないので、セッターはありません。
Recordクラスで保持すべき値はfinal値として扱われるため、利用する場合には(2)のようにコンストラクタに渡します。もしもコンストラクタで値のチェックを行いたい場合には、リスト13のようにも表せます。
public record Point(int x, int y){
public Point{
if(x < 0 || y < 0){
throw new IllegalArgumentException("argument error");
}
}
}
Sealed Class - 継承クラスの作成を限定する
Javaでは、既定でクラスは自由に継承できます。継承してほしくない場合にはfinal指定をすれば継承できませんが、今度はまったく継承ができなくなってしまいます。そこで、開発者の意向で指定したクラスだけ継承させたい、という場合に利用できるのがSealedクラスです。
例えば以下は、図2のようなルールでSealedクラスを定義した例です。
//(1)Sealedクラスを定義
public sealed class Shape
permits Circle,Polygon {}
// 自由に継承して良い場合
public non-sealed class Circle extends Shape{}
// さらに継承できるクラスを指定する
public sealed class Polygon extends Shape
permits Square {}
// 継承できないクラスとする
public final class Square extends Polygon{}
// 実際に利用する場合 (Main.javaからの抜粋)
public void case1(Shape shape){
if(shape instanceof Polygon polygon){
// (省略)
}
else if(shape instanceof Circle circle){
// (省略)
}
else{
// (2)sealedを使うことで、ここにはたどり着かないようにできる
}
}
Sealedクラスでは、sealedによって継承(実装)を限定したものであることを宣言し、permitsによって実装できるクラスを指定します((1))。この例であれば、Circle、PolygonだけがShapeクラスを継承できます。
Sealedクラスを継承した場合、派生クラスの側でも以下のキーワードで継承の可否を宣言しなければなりません。
- sealed(さらに継承可能)
- non-sealed、final(継承不可)
そして、実際に利用する場合、Shapeクラスを継承したクラスは、PolygonまたはPolygonの継承クラス、もしくはCircleクラスしかないため、(2)のブロックには絶対にたどり着かないことが保証できます。これらの機能はライブラリとして提供する場合などで非常に有用で、意図しない継承がなくなるのでライブラリの安全性も高まります。
Sealedインターフェース
インターフェースでもsealedキーワードを使って、実装可能なクラスを制限できます。例えば、図3のようなルールでSealedクラスを定義したものが、リスト15です。
//(1) sealedインターフェースの作成
public sealed interface Shape permits Circle,Square {}
//(2)permitsで指定したクラスしか実装できない
public record Circle(int center,int radius) implements Shape{}
public record Square(int width,int height) implements Shape{}
(1)では、sealed/permitsによる宣言は、クラスの場合と同じです。(2)ではRecordクラスでSealedインターフェースを実装しました。Recordクラスはfinalクラスの扱いなのでnon-sealed、finalなどの宣言は不要です。このようにsealedとrecordを使うことで列挙型(Enum)のような使い方もできるようになります。
まとめ
今回は、Javaコードを記述する開発者が気になる変更点を中心に説明しました。Javaは他の言語に比べて冗長性が高いため、Koltinを好む開発者も増えてきていています。Java自体もこれらの影響を受け、まだまだ言語としても進化しています。そして、これまでよりも簡素にコードを記述できるようになっています。
次回は、今回紹介できなかったJDKのツールや内部構造等に関連する変更点を中心に紹介します。
