0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Geminiの助けを借りて置き去りにされたJavaに追いつきたい:Java 17,21

Posted at

おつかれさまです、みやもとです。

Java学び直し、ようやく今回で最後。
Geminiがピックアップした重要変更は17と21です。

Java 17の変更点

17についてはシールドクラス1つだけでした。

シールドクラス

以下、Geminiの概要解説を引用します。

クラスやインターフェースの継承または実装を、明示的に許可された特定のクラスやインターフェースに制限する仕組みです。
これにより、あるクラス(スーパークラスまたはスーパーインターフェース)が、どのクラスによって継承・実装されるかを厳密に制御できるようになります。

うーん、概念。
この説明だけでどういうものか理解するのはちょっと私には困難ですね。
いつも通りサンプルコードを書いてもらいました。

まずはJava 6のコード。

// Shape.java (Java 6)
/**
 * 図形を表す抽象クラス。
 * 注意: このクラスを継承できるのは Circle と Square のみと想定しています。
 * (しかし、Java 6 ではこれを強制できません)
 */
public abstract class Shape {
    // 共通のメソッドなど (例)
    public abstract double getArea();
}

// Circle.java (Java 6)
public final class Circle extends Shape { // final にしてこれ以上の継承を防ぐ
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

// Square.java (Java 6)
public final class Square extends Shape { // final にしてこれ以上の継承を防ぐ
    private final double side;

    public Square(double side) {
        this.side = side;
    }

    public double getSide() {
        return side;
    }

    @Override
    public double getArea() {
        return side * side;
    }
}

// Main.java (Java 6) - 図形の種類によって処理を分岐する例
public class Main {
    public static void processShape(Shape shape) {
        // instanceof を使って型をチェックし、ダウンキャストする必要がある
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            System.out.println("Circle with radius: " + circle.getRadius() + ", Area: " + circle.getArea());
        } else if (shape instanceof Square) {
            Square square = (Square) shape;
            System.out.println("Square with side: " + square.getSide() + ", Area: " + square.getArea());
        } else {
            // 想定外の Shape サブクラスが渡された場合の処理
            // (Java 6 では未知のサブクラスが作られる可能性があるため、この分岐が必要になることが多い)
            System.out.println("Unknown Shape type. Area: " + shape.getArea());
        }
    }

    public static void main(String[] args) {
        processShape(new Circle(5.0));
        processShape(new Square(4.0));

        // 誰かが勝手に Triangle クラスを作って渡すことも可能 (防げない)
        // class Triangle extends Shape { ... }
        // processShape(new Triangle(...)); // <-- これを防ぐ仕組みがない
    }
}

面積を求めるgetAreaメソッドを定義した抽象クラスShapeと、それを継承するCircleおよびSquareクラスを作ってメインで処理しています。
CircleとSquareをさらに継承することは防いでいるものの、Shapeを継承することについては制限がないので他にも作ろうと思えば継承クラスを作れる状態ですね。
想定している2つの継承クラス以外が渡される可能性も考慮してif文を記述しています。

続いてJava 17バージョン。

// Shape.java (Java 17 Standard)
// このインターフェースを実装できるのは Circle と Square のみ 
public sealed interface Shape permits Circle, Square {
    double getArea();
}

// Circle.java (Java 17 Standard)
// Shape を実装し、final 
public final class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

// Square.java (Java 17 Standard)
// Shape を実装し、final 
public final class Square implements Shape {
    private final double side;

    public Square(double side) {
        this.side = side;
    }

    public double getSide() {
        return side;
    }

    @Override
    public double getArea() {
        return side * side;
    }
}

// Main.java (Java 17 Standard - instanceof のパターンマッチングを使用)
public class Main {

    public static void processShape(Shape shape) {
        // instanceof のパターンマッチング (Java 16で正式導入、Java 17でも標準機能)
        // shape が Circle 型であれば、変数 c にキャストされた状態でブロック内が実行される
        if (shape instanceof Circle c) {
            System.out.println("Circle with radius: " + c.getRadius() + ", Area: " + c.getArea());
        }
        // shape が Square 型であれば、変数 s にキャストされた状態でブロック内が実行される
        else if (shape instanceof Square s) {
            System.out.println("Square with side: " + s.getSide() + ", Area: " + s.getArea());
        }
        // シールドクラスにより、Shape は Circle か Square のいずれかであることが保証されるため、
        // 理論上はここに来ることはない。しかし、将来 Shape に non-sealed なサブクラスが
        // 追加される可能性などを考慮すると、else を書く場合もある。
        // (ただし、現時点の定義では不要とも言える)
        // else {
        //     System.out.println("Unknown sealed shape type.");
        // }
    }

    public static void main(String[] args) {
        Shape circle = new Circle(5.0);
        Shape square = new Square(4.0);

        processShape(circle);
        processShape(square);

        // シールドクラスの制約により、以下はコンパイルエラーになる 
        // final class Triangle implements Shape { ... }
    }
}

Main以外のクラスを見る限り、abstract classの部分がsealed interfaceに変更されたぐらいでそれほど変わったところはありません。
Mainは2箇所変更がありますが、大きいのはprocessShapeのif文につけられていた条件なしelseがコメント化されているところ。
ShapeがCircleとSquareのみに実装を許されているため、それ以外の想定が不要になっています。
コメントに記載の通り、特に実務では今後の拡張に備えて「それ以外」をつけておくことも多いのですが、この時点でそれをやると絶対に通らないコードを実装してテストがちょっと面倒になるため、追加内容を漏れなく反映する限りにおいては書かない方が良いかなという印象です。

ところで、「しかし、将来 Shape に non-sealed なサブクラスが追加される可能性などを…」という箇所。
sealedがnonなので継承に制限がないということなのでしょうが、どういう状況で発生するものでしょうか?
追加で質問してみました。

「将来 Shape に non-sealed なサブクラスが追加される可能性」というのは、Shapeを継承するということでしょうか。
それともCircleやSquareがfinalでなくなり、それらを継承した場合ということですか?

それに対するGeminiさんの答えはこちら。
長いので一部省略します。

「将来 Shape に non-sealed なサブクラスが追加される可能性」というのは、主に以下の2つのシナリオが考えられます。
シナリオ1: Shape インターフェースの定義が変更され、permits 句に non-sealed なクラスが追加される場合
シナリオ2: Circle や Square が final でなくなり、non-sealed に変更される場合

シナリオ1は、permits部分にクラス名を追加して実装可能クラスが増えた上で、増えたクラスがfinalでもsealedでもないために継承したサブクラスの作成が可能な状態。
上記の例ならShapeのpermitsにTriangleを追加し、Triangleクラスをnon-sealedで作成するとTriangleクラスは継承が可能になります。
Triangleクラスもそれを継承したサブクラスもprocessShape内では条件なしのelseに入ることになります。
ただ、個人的には「わざわざpermits指定を増やしてまで作ったクラスをnon-sealedで作るか?」という気も。

どちらかというとシナリオ2の「CircleやSquareがfinalでなくなり、non-sealedに変更される場合」という方がありそうかも、という感じ。
sealed interfaceの指定自体意味が無くなる気がするのでこれはこれで可能性低いようにも思いますが、「これ継承して使った方がコード綺麗になるのでは?」と判断されて継承可能にするとかはまだ想像できなくもない。

なお、シールド指定したインターフェースを実装するクラスは宣言時に以下の指定のいずれかが必要になります。

  • final : サブクラスを作成させない
  • sealed : permitsで指定したクラスのみ作成可能にする
  • non-sealed : サブクラスの作成を制限しない

試しに先述のサンプルコードでSquareからfinalを削除したらエラーになりました。
うっかり指定漏れしておかしな動きをすることはなさそうですね。

Java 21の変更点

続いてはJava 21の変更点2つ。

switch文のパターンマッチング

switch文で判定できる範囲が増えました。
最初は文字列すらエラーになっていたというのに…感慨深い気持ちです。

さっそくサンプルコードいきましょう。
Java 7でString対応をピックアップしたからか、ここだけJava 6と21の間に7のサンプルコードも書いてくれました。

ではまずJava 6の場合。

// Java 6 (switch文で文字列を使う場合は、if-else ifの連鎖が必要)
public class Java6Switch {
    public static void main(String[] args) {
		String day = "Mon";
        String typeOfDay = "";
        if (day.equals("Mon") || day.equals("Tue") || day.equals("Wed") || day.equals("Thu") || day.equals("Fri")) {
            typeOfDay = "Weekday";
        } else if (day.equals("Sat") || day.equals("Sun")) {
            typeOfDay = "Weekend";
        } else {
            typeOfDay = "Unknown";
        }
		System.out.println(typeOfDay);
    }
}

Java 7の変更点でサンプル書いてくれた時はEnum使ってましたが、今度はif文で書いてくれました。
Geminiさんの気分の問題でしょうか。
「switch文で文字列を使う場合は…」と、あたかもswitch文書いてますよみたいな雰囲気でif文使ってるのは若干騙された感しなくもないですね?
まぁ、実際わざわざEnum使ってswitch case切るよりはif文を連結して書く方がよく見かける気がします。

続きましてJava 7の場合。

//Java7 以降はこのように書けます。
public class Java7Switch {
    public static void main(String[] args) {
		String day = "Mon";
        String typeOfDay;
		switch (day) {
			case "Mon":
			case "Tue":
			case "Wed":
			case "Thu":
			case "Fri":
				typeOfDay = "Weekday";
				break;
			case "Sat":
			case "Sun":
				typeOfDay = "Weekend";
				break;
			default:
				typeOfDay = "Unknown";
				break;
		}
		System.out.println(typeOfDay);
    }
}

同じ処理をするcaseは連続して書いて処理をまとめ、処理が終わったらbreakで抜ける。
見慣れたswitch文ですね。

最後に今回の変更、Java 21の場合。

// Java 21 (Pattern Matching for switch)
public class Java21Switch {
    public static void main(String[] args) {
		String day = "Mon";

        String typeOfDay = switch (day) {
            case "Mon", "Tue", "Wed", "Thu", "Fri" -> "Weekday";
            case "Sat", "Sun" -> "Weekend";
            default -> "Unknown";
        };
		System.out.println(typeOfDay);
    }
}

Java 7と同様に同じ処理を行う値をまとめていますが、caseを縦に並べるのではなく値をひとつのcase文に続けて書いています。
あとラムダ式の時に出てきたアロー演算子が出てきましたね。
ラムダ式の時はアロー演算子の左が引数、右が処理になっていました。
今回はアロー演算子の左側は引数というか、「引数の値がこれだったら」という条件ですかね?

ところで、またパターンマッチングの意味増えてないですか。
instanceof演算子の時の「型と一致するかどうかを判定し、一致する場合はキャストして変数に取り出す」ともまた違いません?

switch文のパターンマッチングについて

さっそくGeminiに聞いたところ、詳しい解説と別のサンプルが返ってきました。
質問のプロンプトはこんな感じ。

Javaについて、switch文のパターンマッチングは「例示された値の中に一致するものがあるかどうか」という認識で合っていますか?

そして回答。
長いのでこれも一部抜粋して引用します。

単純な値の一致だけでなく、型、null、追加条件を含む、より複雑なパターンに基づいて分岐処理を行うための機能拡張です。あなたの「例示された値の中に一致するものがあるかどうか」という認識は、主に定数パターンの側面を指していますが、switch のパターンマッチングは型パターンによる型チェックも可能であり、instanceof のパターンマッチングと同様の機能も内包しています。

Object obj = 123; // または "Java", null, new Circle(10), etc.

// switch のパターンマッチング (Java 21で標準機能)
String result = switch (obj) {
    case String s         -> "String: " + s;
    case Integer i        -> "Integer: " + i;
    case null             -> "It's null"; // null チェック
    case Double d when d > 0.0 -> "Positive double: " + d; // ガード付きパターン
    default               -> "Other type";
};
System.out.println(result);

たとえばひとつ目のcase、objがStringなら変数sにキャストしてアロー演算子の右辺で処理しています。
左辺の動きはinstanceof演算子で見たパターンパッチングと同じ記述ですね。
他にも4つ目のcaseではobjがDoubleならさらに0.0より大きいかを判定した上で処理していて、結構処理の幅が広そうです。

Unnamed Classes and Instance Main Methods

いきなり英文なのは、いまいちしっくり来る訳を見つけられなかったせいです。
ついでに言うとこの機能はPreviewなので、厳密にはJava 21から正式実装された機能ではありません。
ただ初めて聞いたときにめちゃくちゃ衝撃的だったので書くことにしました。

どんな変更かはサンプルコードを見ていただけるとわかると思います。
まずJava 6。

//Java21 以前
public class Java20Main{
	public static void main(String[] args){
		System.out.println("Hello, World!");
	}
}

よくあるHello, World!ですね。
続いてJava 21。

// Java 21 (Unnamed Classes and Instance Main Methods)
void main() {
    System.out.println("Hello, World!");
}

この衝撃お分かりいただけるでしょうか。
public static void main(string[] args)じゃない!!
ついでにpublic classも無い!

まだ新卒で初めてJava研修を受けて以来ずっと決まり文句だったpublic static void main(string[] args)。
実務ではIDEが補完してくれるので特に気にしていなかったのですが、初学者にはちょっと面倒と思われがちなこれを簡略化するために導入されたそうです。
効率とか可読性とかでなく、学習のハードルを下げるための機能実装というのはなかなか新鮮な気がしますね。

Previewとは?

Previewとは、製品品質で実装されているが今後の実装が確定されていない機能。
詳細は以下のリンクをご参照ください。

プレビュー機能を使う場合、コンパイラとランタイムで機能を明示的に有効にする必要があります。
IntelliJ IDEAで上記サンプルコードを実行した際、最後のコードはペースト時点でエラーが出て「プレビュー版の実行環境にしますか?」という旨のメッセージが表示されました。
そこだけ対応すれば普通に新機能を試せるのはなかなかお手軽ですね。

まとめ

改めて変更点を勉強したことで、Java 6以降の追加機能の大きさを実感しました。
最近はJavaも比較的古い言語に分類されている気がしますが、いろいろ変化して新しい技術に対応していく気概を感じますね。

また、AI相手だと次々湧いてくる疑問を深掘りするのもお手軽にできるので自習するのに良いなーと感じました。
まるきり新規の技術になると私が適切なデータソースを選定できるかという課題がありますが、ある程度知識があるものの学び直しならいける気がします。
今後もちょくちょく「Javaの追加機能教えてー」とかやって、進化するJavaを追いかけていこうかと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?