LoginSignup
13
9

レコードとかswitch式とかパターンマッチとかテキストブロックとかその辺のまとめ

Posted at

Java 11 から Java 21 までの間に追加された以下の言語仕様について、一通り正式採用されたっぽくて LTS も出たので整理する。

  • switch式
  • テキストブロック
  • Pattern Matching for instanceof
  • レコードクラス
  • シールクラス
  • レコードパターン
  • Pattern Matching for switch

環境

> java --version
openjdk 21.0.1 2023-10-17 LTS
OpenJDK Runtime Environment Temurin-21.0.1+12 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.1+12 (build 21.0.1+12-LTS, mixed mode, sharing)

各言語仕様の正式採用バージョン整理

バージョン 正式採用された言語仕様
11
12
13
14 switch式
15 テキストブロック
16 Pattern Matching for instanceof
レコードクラス
17 シールクラス
18
19
20
21 レコードパターン
Pattern Matching for switch

レコードクラス(Record Class)

DDD の値クラスみたいなのを簡単に作れるようにしたもの。
アクセサや toString, equals の実装など、ありがちなボイラープレートを書かずに簡単にデータを入れるためのクラスを作成できる。

Hello World

public record MyRecord(int number, String text) {
}
public class RecordTest {

    public static void main(String[] args) {
        MyRecord myRecord = new MyRecord(1, "test");
        System.out.println(myRecord);
    }
}
実行結果
MyRecord[number=1, text=test]

説明

public record MyRecord(int number, String text) {
}
  • レコードを定義する場合は、 class の代わりに record で型を定義する
  • クラス名の直後に、レコードに持たせるフィールドを宣言する
  • 以上のコードで、以下のクラスを定義したのと同じになる
Recordで生成されるクラス
public final class MyRecord {
    private final int number;
    private final String text;

    public MyRecord(int number, String text) {
        this.number = number;
        this.text = text;
    }

    public int number() {
        return number;
    }

    public String text() {
        return text;
    }

    @Override
    public String toString() {
        return "MyRecord[number=" + number + ", text=" + text + "]";
    }

    @Override
    public int hashCode() {
        // number, text の値を元にハッシュ値を計算
    }

    @Override
    public boolean equals(Object other) {
        // 型が等しく、number, textが等しい場合はtrueを返す
    }
}
  • 以下の実装が自動的に定義される
    • final なクラス
    • レコードコンポーネントに対応する final なフィールド
    • フィールドの値を全て受け取り初期化するコンストラクタ
    • 各フィールドと同名のゲッター
    • 全てのフィールドを名前と共に出力する toString
    • 全てのフィールドでハッシュ値を計算する hashCode
    • 型が等しく全てのフィールドの値が等しい場合に true を返す equals
  • フィールドの宣言は空にすることもできる(実用性はなさそうだが)
public record MyRecord() {
}
  • この場合でも () は省略できない(() を省略するとコンパイルエラー)

コンパクト・コンストラクタ

public record MyRecord(int number, String text) {
    public MyRecord {
        System.out.println("Compact Constructor : number=" + number + ", text=" + text);
    }
}
public class RecordTest {

    public static void main(String[] args) {
        new MyRecord(10, "test");
    }
}
実行結果
Compact Constructor : number=10, text=test
  • 仮引数を省略する形でコンストラクタを宣言できる
  • これを コンパクト・コンストラクタ と呼ぶ
  • コンパクト・コンストラクタは、以下のような実装から最後のフィールドへの代入を省略したものと同じになる
コンパクト・コンストラクタと同等のコンストラクタ
public MyRecord(int number, String text) {
    ...
    this.number = number;
    this.text = text;
}

コンストラクタをオーバーロードできる

public record MyRecord(int number, String text) {
    
    public MyRecord(int number) {
        this(number, "MyRecord(int)");
    }

    public MyRecord(String text) {
        this(-1, text);
    }

    public MyRecord() {
        this(-1, "MyRecord()");
    }
}
  • コンストラクタはオーバーロードが可能
  • コンパクト・コンストラクタとの併用も可能

static 要素を宣言できる

public record MyRecord(String text) {

    static {
        System.out.println("Static Initializer");
    }

    public static void staticMethod() {
        System.out.println("Static Method. STATIC_FIELD=" + STATIC_FIELD);
    }

    public final static String STATIC_FIELD = "Static Field";
}
public class RecordTest {

    public static void main(String[] args) {
        MyRecord.staticMethod();
    }
}
実行結果
Static Initializer
Static Method. STATIC_FIELD=Static Field
  • static イニシャライザ、static メソッド、static フィールドは通常のクラスと同様に宣言できる

インスタンス・イニシャライザは宣言できない

コンパイルエラー
public record MyRecord(String value) {
    {
        System.out.println("これは実装できない");
    }
}
  • レコードクラスではインスタンス・イニシャライザは宣言できない

自動生成されるメソッドをオーバーライドできる

public record MyRecord(String text) {

    @Override
    public String text() {
        return "<<" + text + ">>";
    }
}
public class RecordTest {

    public static void main(String[] args) {
        MyRecord myRecord = new MyRecord("test");
        System.out.println(myRecord.text());
    }
}
実行結果
<<test>>
  • アクセサや toString など、自動生成されるメソッドは自由にオーバーライドできる

任意のメソッドを定義できる

public record MyRecord(String text) {

    public void hello() {
        System.out.println("Hello " + text);
    }
}
public class RecordTest {

    public static void main(String[] args) {
        MyRecord myRecord = new MyRecord("test");
        myRecord.hello();
    }
}
実行結果
Hello test
  • 自動生成されるメソッド以外でも自由にメソッドを追加できる

総称型にできる

public record MyRecord<T> (T value) {
}
public class RecordTest {

    public static void main(String[] args) {
        MyRecord<String> myRecord = new MyRecord<>("test");
        System.out.println(myRecord);
    }
}
実行結果
MyRecord[value=test]
  • レコードクラスは総称型にできる

インタフェースを実装できる

public record MyRecord(String value) implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRecord.run()");
    }
}
public class RecordTest {

    public static void main(String[] args) {
        Runnable myRecord = new MyRecord("test");
        myRecord.run();
    }
}
実行結果
MyRecord.run()
  • レコードクラスは通常のクラス同様にインタフェースを実装できる

クラスの継承はできない

コンパイルエラー
public record MyRecord(String value) extends Number {
}
  • レコードクラスは他のクラスを継承することはできない

レコードコンポーネントとアノテーション

MyFieldAnnotation
package java21.record;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyFieldAnnotation {
}
  • TargetFIELD を設定したアノテーション
MyRecordComponentAnnotation
package java21.record;

...

@Target(ElementType.RECORD_COMPONENT)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRecordComponentAnnotation {
}
  • TargetRECORD_COMPONENT を設定したアノテーション
MyMethodAnnotation
package java21.record;

...

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
}
  • TargetMETHOD を設定したアノテーション
MyParameterAnnotation
package java21.record;

...

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyParameterAnnotation {
}
  • TargetPARAMETER を設定したアノテーション
MyTypeUseAnnotation
package java21.record;

...

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTypeUseAnnotation {
}
  • TargetTYPE_USE を設定したアノテーション
MyRecord
public record MyRecord(
        @MyFieldAnnotation
        @MyRecordComponentAnnotation
        @MyParameterAnnotation
        @MyMethodAnnotation
        @MyTypeUseAnnotation
        String value) {
}
  • レコードコンポーネントに、上で定義した5つのアノテーションを設定している
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.RecordComponent;
import java.util.Arrays;

public class RecordTest {

    public static void main(String[] args) throws Exception {
        Field field = MyRecord.class.getDeclaredField("value");
        printAnnotations("Field", field, field.getAnnotatedType());

        for (RecordComponent recordComponent : MyRecord.class.getRecordComponents()) {
            printAnnotations("RecordComponent", recordComponent,
                    recordComponent.getAnnotatedType());
        }

        Constructor<MyRecord> constructor = MyRecord.class.getConstructor(String.class);
        for (Parameter parameter : constructor.getParameters()) {
            printAnnotations("Constructor.Parameter", parameter,
                    parameter.getAnnotatedType());
        }

        Method valueAccessor = MyRecord.class.getMethod("value");
        printAnnotations("Method", valueAccessor,
                valueAccessor.getAnnotatedReturnType());
    }

    private static void printAnnotations(String tag, AnnotatedElement ae,
                                         AnnotatedType annotatedType) {
        System.out.println("=== " + tag + " ===");
        System.out.println("  annotations = " + Arrays.toString(ae.getAnnotations()));
        System.out.println("  annotatedType.annotations = "
                + annotatedTypeAnnotations(annotatedType));
    }

    private static String annotatedTypeAnnotations(AnnotatedType annotatedType) {
        return Arrays.toString(annotatedType.getAnnotations());
    }
}
  • MyRecord からリフレクションを使って各アノテーションの情報を出力している
実行結果
=== Field ===
  annotations = [@java21.record.MyFieldAnnotation()]
  annotatedType.annotations = [@java21.record.MyTypeUseAnnotation()]
=== RecordComponent ===
  annotations = [@java21.record.MyRecordComponentAnnotation()]
  annotatedType.annotations = [@java21.record.MyTypeUseAnnotation()]
=== Constructor.Parameter ===
  annotations = [@java21.record.MyParameterAnnotation()]
  annotatedType.annotations = [@java21.record.MyTypeUseAnnotation()]
=== Method ===
  annotations = [@java21.record.MyMethodAnnotation()]
  annotatedType.annotations = [@java21.record.MyTypeUseAnnotation()]

説明

public record MyRecord(
        @MyFieldAnnotation
        @MyRecordComponentAnnotation
        @MyParameterAnnotation
        @MyMethodAnnotation
        @MyTypeUseAnnotation
        String value) {
}
  • レコードコンポーネントには、 @Target で以下のいずれかを指定したアノテーションを設定できる
    • FIELD
    • RECORD_COMPONENT
    • PARAMETER
    • METHOD
    • TYPE_USE
  • 設定されたアノテーションは、自動生成される各要素に分配される
    • FIELD
      • 自動生成されるフィールドに設定される
    • RECORD_COMPONENT
      • レコードコンポーネントにだけ設定される
    • PARAMETER
      • 自動生成されるコンストラクタのパラメータに設定される
    • METHOD
      • 自動生成されるアクセサに設定される
    • TYPE_USE
      • 自動生成される各要素の型宣言部分に設定される
  • アノテーションの展開のイメージは、おそらく以下のような感じ
    • あくまでイメージ
public class MyRecord (@MyRecordComponentAnnotation @MyTypeUseAnnotation String value) {
    @MyFieldAnnotation
    @MyTypeUseAnnotation
    private final String value;

    public MyRecord(@MyParameterAnnotation @MyTypeUseAnnotation String value) {
        this.value = value;
    }

    @MyMethodAnnotation
    @MyTypeUseAnnotation
    public String value() {
        return value;
    }
}

ミュータブルなコンポーネントを宣言した場合

import java.util.List;

public record MyRecord(List<String> list) {
}
import java.util.ArrayList;
import java.util.List;

public class RecordTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("foo", "bar"));
        MyRecord myRecord = new MyRecord(list);
        System.out.println("before : " + myRecord);
        list.add("FIZZ");
        System.out.println("after : " + myRecord);
    }
}
実行結果
before : MyRecord[list=[foo, bar]]
after : MyRecord[list=[foo, bar, FIZZ]]

フィールドが final で宣言されてセッターも宣言されないので一見イミュータブルっぽいが、コンポーネントをミュータブルにしていると外部から状態を変更できるので注意が必要。

switch式

Hello World

public class SwitchExpressionTest {
    public static void main(String[] args) {
        for (int i=0; i<6; i++) {
            try {
                System.out.println(hello(i));
            } catch (IllegalArgumentException e) {
                System.out.println("e.message=" + e.getMessage());
            }
        }
    }

    private static String hello(int n) {
        return switch (n) {
            case 0 -> "ZERO";
            case 1, 2 -> "ONE or TWO";
            case 3 -> throw new IllegalArgumentException("n=3");
            case 4 -> {
                System.out.print("n=4 ");
                yield "FOUR";
            }
            default -> "OTHER";
        };
    }
}
実行結果
ZERO
ONE or TWO
ONE or TWO
e.message=n=3
n=4 FOUR
OTHER

説明

return switch (n) {
    case 0 -> "ZERO";
    case 1, 2 -> "ONE or TWO";
    case 3 -> throw new IllegalArgumentException("n=3");
    case 4 -> {
        System.out.print("n=4 ");
        yield "FOUR";
    }
    default -> "OTHER";
};
  • switch式は、名前の通り switch が式として評価されて値を返す
  • case ラベルが従来の switch文と異なり以下のように書ける
    • case label[, label...] -> expression; | throw-statement; | block
  • 1つの case の中でカンマ区切りで複数のラベルを列挙できる
  • フォール・スルーを防ぐための break は不要
  • 複数行のコードを書きたい場合はブロックを書ける
    • その場合、 switch式の結果となる値は yield 値 という形で記述する
  • 入力された値が取りえる値を全てラベルとして列挙する必要があるため、基本的に default ラベルは必須となる

null ケースラベル

public class SwitchExpressionTest {
    public static void main(String[] args) {
        System.out.println(hello(null));
    }

    private static String hello(String s) {
        return switch (s) {
            case "a" -> "A";
            default -> "DEFAULT";
        };
    }
}
実行結果
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local1>" is null
	at java21.switchexp.SwitchExpressionTest.hello(SwitchExpressionTest.java:9)
	at java21.switchexp.SwitchExpressionTest.main(SwitchExpressionTest.java:5)
  • default ラベルがあっても、 null を渡すと実行時に NullPointerException になる
  • null の場合も処理したい場合は、nullケースラベルを追加する
public class SwitchExpressionTest {
    public static void main(String[] args) {
        System.out.println(hello(null));
    }

    private static String hello(String s) {
        return switch (s) {
            case "a" -> "A";
            case null -> "NULL"; // ★nullケースラベル
            default -> "DEFAULT";
        };
    }
}
実行結果
NULL
  • case null -> というケースを追加することで null をハンドリングできる
  • null とデフォルトを同じ操作にしたい場合は、以下のように書く
public class SwitchExpressionTest {
    public static void main(String[] args) {
        System.out.println(hello(null));
    }

    private static String hello(String s) {
        return switch (s) {
            case "a" -> "A";
            case null, default -> "DEFAULT";
        };
    }
}
実行結果
DEFAULT
  • case null, default のように、 defaultcase のラベルに指定できるようになっている

列挙型を渡す場合

public class SwitchExpressionTest {
    public static void main(String[] args) {
        System.out.println(hello(MyEnum.HOGE));
    }

    private static String hello(MyEnum e) {
        return switch (e) {
            case HOGE -> "hoge";
            case FUGA -> "fuga";
            case PIYO -> "piyo";
        };
    }

    enum MyEnum {
        HOGE,
        FUGA,
        PIYO,
    }
}
実行結果
hoge
  • switch式に列挙型の値を渡す場合、全ての列挙子をラベルに挙げれば default ラベルは不要となる

シールクラス (Sealed Classes)

英語だと Sealed Classes だけど、日本語表記だとシールクラスになるっぽい。
(シールドだと shield(盾) のイメージが強いから? seal は「密封、密閉」といった意味)

シール・クラス | Java言語更新

Hello World

MyClass
public sealed class MyClass permits Hoge, Fuga{
}
Hoge
public final class Hoge extends MyClass {
}
Fuga
public final class Fuga extends MyClass {
}
Piyo(コンパイルエラー)
// 'Piyo' is not allowed in the sealed hierarchy
public final class Piyo extends MyClass {
}
  • クラスの修飾子に sealed を付けることで、そのクラスをシールクラスにできる
  • シールクラスは、そのクラスを継承できるクラスを限定できる
    • クラス名の後の permits で、継承できるクラスを列挙する
    • ここでは Hoge, Fuga クラスだけが継承できるように許可している
    • Piyo は継承を許可されていないので、 MyClass を継承しようとするとコンパイルエラーになる
  • permits で継承を許可されたクラスはシールクラスを継承できる
    • シールクラスを継承したクラスは、以下のいずれかの修飾子を付ける必要がある
    • final
      • それ以上の継承を許さない場合は final をつける
    • sealed
      • さらにサブクラスの型を限定する場合は sealed をつける
    • non-sealed
      • サブクラスの型を限定しない場合は non-sealed をつける

継承を許可するクラスを配置できる場所

  • permits に列挙するクラスは、コンパイル時にシールクラスからアクセス可能な場所に存在しなければならない
  • また、シールクラスが名前付きモジュールの中に存在する場合は、許可されるクラスは全て同じモジュール内にいなければならない
  • シールクラスが名前付きモジュールの中に存在しない場合は、許可されるクラスは全て同じパッケージ内に存在する必要がある
フォルダ構成
src/main/java/
  `-java21/sealed/
    |-MyClass.java
    |-Hoge.java
    `-sub/
      `-Foo.java
MyClass(コンパイルエラー)
package java21.sealed;

// Class is not allowed to extend sealed class from another package
public sealed class MyClass permits Hoge, java21.sealed.sub.Foo {
}
Hoge
package java21.sealed;

public final class Hoge extends MyClass {
}
Foo
package java21.sealed.sub;

import java21.sealed.MyClass;

public final class Foo extends MyClass {
}
  • シールクラスが存在するプロジェクトには module-info.java がないので、無名モジュールとなっている
  • シールクラスである MyClass は、異なるパッケージに存在する Foopermits に指定している
  • しかし、名前付きモジュールでない場合は別パッケージのクラスを permits に指定できないので、コンパイルエラーとなっている
  • これのコンパイルを通すためには、シールクラスである MyClass が存在するプロジェクトを名前付きモジュールにする必要がある
フォルダ構成
src/main/java/
  |-module-info.java ★追加
  `-java21/sealed/
    |-MyClass.java
    |-Hoge.java
    `-sub/
      `-Foo.java
module-info.java
module test {
}
  • これでコンパイルエラーが無くなる

sealed を連鎖させる

MyClass
public sealed class MyClass permits Hoge {
}
Hoge
public sealed class Hoge extends MyClass permits Fuga, Piyo {
}
Fuga
public final class Fuga extends Hoge {
}
Piyo
public final class Piyo extends Hoge {
}
  • permits で継承を許可したクラスに sealed を付けることで、さらにそのサブタイプを限定させることができる

任意のクラスが継承できるようにする

MyClass
public sealed class MyClass permits Hoge, Fuga {
}
Hoge
public final class Hoge extends MyClass {
}
Fuga
public non-sealed class Fuga extends MyClass {
}
Foo
public class Foo extends Fuga {
}
  • permits で継承を許可したクラスに non-sealed を付けると、そのクラスは任意のクラスで継承できるようになる

シールインタフェース

MyInterface
public sealed interface MyInterface permits Hoge, MyRecord {
}
Hoge
public final class Hoge implements MyInterface {
}
MyRecord
public record MyRecord() implements MyInterface {
}
Piyo(コンパイルエラー)
// 'Piyo' is not allowed in the sealed hierarchy
public class Piyo implements MyInterface {
}
  • インタフェースにも sealed を付けて継承を制限させられる
  • レコードクラスを permits に指定することも可能

Pattern Matching for instanceof

Hello World

public class PatternMatchForInstanceof {

    public static void main(String[] args) {
        patternMatch("string");
        patternMatch(new Exception("exception"));
    }

    private static void patternMatch(Object obj) {
        if (obj instanceof String s) {
            System.out.println("s.upperCase = " + s.toUpperCase());
        } else if (obj instanceof Exception e) {
            System.out.println("e.message = " + e.getMessage());
        }
    }
}
実行結果
s.upperCase = STRING
e.message = exception

説明

    private static void patternMatch(Object obj) {
        if (obj instanceof String s) {
            System.out.println("s.upperCase = " + s.toUpperCase());
        } else if (obj instanceof Exception e) {
            System.out.println("e.message = " + e.getMessage());
        }
    }
  • instanceof による型比較の後ろに変数を宣言できる
    • 上の例だと s, e が該当する
    • この変数をパターン変数と呼ぶ
  • instanceof による比較が真だった場合、比較対象の値がパターン変数に代入されて利用できるようになる
  • これにより、 instanceof で比較した後の明示的なキャストが不要となる

パターン変数のスコープ

    private static void patternMatch(Object obj) {
        if (obj instanceof String s) {
            System.out.println(s);
            System.out.println(e); // コンパイルエラー
        } else if (obj instanceof Exception e) {
            System.out.println(s); // コンパイルエラー
            System.out.println(e);
        } else {
            System.out.println(s); // コンパイルエラー
            System.out.println(e); // コンパイルエラー
        }

        System.out.println(s); // コンパイルエラー
        System.out.println(e); // コンパイルエラー
    }
  • パターン変数が参照できるのは、 instanceof による比較が true となったときに通る場所に限られる
  • したがって、次のように if 条件式の中で続けて参照することもできる
    private static void patternMatch(Object obj) {
        if (obj instanceof String s && s.length() < 10) {
            System.out.println(s);
        }
    }
  • 条件を && でつなげているため、 s.length() < 10 という形で同じ if 条件式の中で参照できる
    • s.length() < 10s instanceof String が真である場合に評価されるため
  • 一方で、以下のようなケースはコンパイルエラーになる
コンパイルエラー
    private static void patternMatch(Object obj) {
        // Cannot resolve symbol 's'
        if (obj instanceof String s || s.length() < 10) {
            System.out.println(s);
        }
    }
  • 条件が || でつなげられているので、 s.length() < 10obj instanceof String が偽の場合も実行される可能性があり、パターン変数は参照できない
  • instanceof の結果が真のときに参照できるという条件なので、次のような形でも書ける
    private static void patternMatch(Object obj) {
        if (!(obj instanceof String s)) {
            System.out.println(s); // コンパイルエラー
            return;
        }
        System.out.println(s); // 参照可能
    }
  • if の条件は ! で否定しているので、パターン変数 s が参照できるのは条件が偽となる if の外になる

Pattern Matching for instanceof が書ける場所

例では if の条件式で書いていたが、これ自体は最終的に boolean として評価される式なので、式を書ける場所なら書ける。

    private static void patternMatch(Object obj) {
        String text = (obj instanceof String s) ? s.toUpperCase() : "DEFAULT";
        boolean b = (obj instanceof Exception e);
    }
  • 1つ目は三項演算子の条件のところに、2つ目は boolean 変数の初期化に Pattern Matching for instanceof を記述している
    • 2つ目はパターン変数を使っていないのでこう書く意味は全く無い
    • あくまで書こうと思えば書けることを確認しているだけ

Pattern Matching for switch

Hello World

public class PatternMatchForInstanceof {

    public static void main(String[] args) {
        System.out.println(patternMatch("string"));
        System.out.println(patternMatch(new Exception("exception")));
        System.out.println(patternMatch(10));
    }

    private static String patternMatch(Object obj) {
        return switch (obj) {
            case String s -> s.toUpperCase();
            case Exception e -> e.getMessage();
            default -> "DEFAULT(" + obj + ")";
        };
    }
}
実行結果
STRING
exception
DEFAULT(10)

説明

    private static String patternMatch(Object obj) {
        return switch (obj) {
            case String s -> s.toUpperCase();
            case Exception e -> e.getMessage();
            default -> "DEFAULT(" + obj + ")";
        };
    }
  • switch のラベル部分でパターンマッチを記述できる
  • case 型 変数 と書くことで、 switch に渡した値の型でケースを分けることができる
  • 型が一致した場合は変数に代入され、そのケースの中だけで利用できるようになる

when句で条件を絞る

public class PatternMatchForInstanceof {

    public static void main(String[] args) {
        System.out.println(patternMatch("one"));
        System.out.println(patternMatch("two"));
        System.out.println(patternMatch("three"));
    }

    private static String patternMatch(Object obj) {
        return switch (obj) {
            case String s when s.length() < 4 -> s;
            case String s -> s.substring(0, 3) + "...";
            default -> "DEFAULT(" + obj + ")";
        };
    }
}
実行結果
one
two
thr...
  • case 型 変数 when 条件式 のように記述することで、型以外の任意の条件を追加できる
  • 上の実装の場合、 objString でかつ長さが4文字未満であれば最初のケースにマッチし、4文字以上の String は2つ目のケースにマッチする
  • ちなみに、1つ目と2つ目のケースを逆にするとコンパイルエラーになる
コンパイルエラー
    private static String patternMatch(Object obj) {
        return switch (obj) {
            case String s -> s.substring(0, 3) + "...";
            // Label is dominated by a preceding case label 'String s'
            case String s when s.length() < 4 -> s;
            default -> "DEFAULT(" + obj + ")";
        };
    }
  • 1つ目の条件があることで2つ目の条件は常に除外されるようになるため、コンパイルエラーとなっている

シールクラスのパターンマッチ

MySealedClass
public sealed class MySealedClass permits Hoge, Fuga {
}
Hoge
public final class Hoge extends MySealedClass {
}
Fuga
public final class Fuga extends MySealedClass {
}
public class SwitchExpressionTest {

    public static void main(String[] args) {
        System.out.println(patternMatch(new MySealedClass()));
        System.out.println(patternMatch(new Hoge()));
        System.out.println(patternMatch(new Fuga()));
    }

    private static String patternMatch(MySealedClass msc) {
        return switch (msc) {
            case Hoge h -> "Hoge";
            case Fuga f -> "Fuga";
            case MySealedClass m -> "MySealedClass";
        };
    }
}
実行結果
MySealedClass
Hoge
Fuga
  • シールクラスを switch 式に渡してパターンマッチをした場合、可能性のある型を全て網羅できる形でケースを列挙すれば default ケースが不要となる
  • 網羅できていないと、コンパイルエラーになる
コンパイルエラー
    private static String patternMatch(MySealedClass msc) {
        return switch (msc) {
            case Hoge h -> "Hoge";
            case Fuga f -> "Fuga";
            // MySealedClass が来た場合が網羅されていないのでコンパイルエラー
        };
    }
コンパイルOK
    private static String patternMatch(MySealedClass msc) {
        return switch (msc) {
            // Hoge, Fuga が来た場合も MySealedClass として処理されるので網羅できている
            case MySealedClass m -> "MySealedClass";
        };
    }

non-sealed が含まれる場合

MySealedClass
public sealed class MySealedClass permits Hoge, Fuga {
}
Hoge
public final class Hoge extends MySealedClass {
}
Fuga
public non-sealed class Fuga extends MySealedClass {
}
Piyo
public class Piyo extends Fuga {
}
  • Fuganon-sealed にして Piyo をサブクラスとして追加している
  • この状態にしても、先ほどのパターンマッチの実装はコンパイルが通る
コンパイルは通る
public class PatternMatchForInstanceof {

    public static void main(String[] args) {
        System.out.println(patternMatch(new MySealedClass()));
        System.out.println(patternMatch(new Hoge()));
        System.out.println(patternMatch(new Fuga()));
        System.out.println(patternMatch(new Piyo())); // ★Piyo を渡すケースを追加
    }

    private static String patternMatch(MySealedClass msc) {
        return switch (msc) {
            case Hoge h -> "Hoge";
            case Fuga f -> "Fuga";
            case MySealedClass m -> "MySealedClass";
        };
    }
}
実行結果
MySealedClass
Hoge
Fuga
Fuga
  • PiyoFuga のサブタイプなので、 case Fuga f のパターンにマッチして処理されている
  • ちなみに、 Piyo のパターンのラベルを追加することもできる
public class PatternMatchForInstanceof {

    public static void main(String[] args) {
        System.out.println(patternMatch(new MySealedClass()));
        System.out.println(patternMatch(new Hoge()));
        System.out.println(patternMatch(new Fuga()));
        System.out.println(patternMatch(new Piyo()));
    }

    private static String patternMatch(MySealedClass msc) {
        return switch (msc) {
            case Hoge h -> "Hoge";
            case Piyo p -> "Piyo"; // ★Piyo のラベルを追加
            case Fuga f -> "Fuga";
            case MySealedClass m -> "MySealedClass";
        };
    }
}
実行結果
MySealedClass
Hoge
Fuga
Piyo

レコードパターン

Hello World

MyRecord
public record MyRecord(int i, String s) {
    @Override
    public int i() {
        System.out.println("i()");
        return i;
    }

    @Override
    public String s() {
        System.out.println("s()");
        return s;
    }
}
public class RecordPatternTest {
    public static void main(String[] args) {
        MyRecord record = new MyRecord(1, "test");

        System.out.println(recordPattern(record));
    }

    private static String recordPattern(Object obj) {
        if (obj instanceof MyRecord(int i, String s)) {
            return "i=" + i + ", s=" + s;
        } else {
            return "default";
        }
    }
}
実行結果
i()
s()
i=1, s=test

説明

    private static String recordPattern(Object obj) {
        if (obj instanceof MyRecord(int i, String s)) {
            return "i=" + i + ", s=" + s;
        } else {
            return "default";
        }
    }
  • instanceof でレコードの型を判定したときに、レコードのコンポーネントを分解して抽出できる
  • レコードの型名の後ろに () を付けて抽出するコンポーネントを列挙することで抽出できる(これをパターンリストと呼ぶ)
    • 各パターンは、型名と変数名のセットで宣言する
  • コンポーネントの値は、アクセサを経由して取得される
  • パターンリストには、レコードクラスに定義されているコンポーネントを同じ順序で全て列挙する必要がある
    • 一致しない場合はコンパイルエラー
  • 抽出したコンポーネントは、 instanceof が真と判定される範囲内で参照できるようになる

型推論

    private static String recordPattern(Object obj) {
        if (obj instanceof MyRecord(var i, var s)) {
            return "i=" + i + ", s=" + s;
        } else {
            return "default";
        }
    }
  • 各パターンの型には var が利用できる
  • これにより、型の記述を省略できる
  • レコードクラスの構造は明確に定義されているので、ここで var を使うのはアリなのかなと個人的には思う
    • なんの型の値が入っているかぱっと見で分からない場所で var を使うのは危険だと思う
    • しかし、ここはレコードクラスのコンポーネントであることがハッキリしているので、型は人間にも推測できると思う(私見)

レコードパターンのネスト

Hoge
public record HogeRecord(double d, FugaRecord fuga) {
}
Fuga
public record FugaRecord(int i, String s) {
}
public class RecordPatternTest {
    public static void main(String[] args) {
        FugaRecord fuga = new FugaRecord(9, "test");
        HogeRecord hoge = new HogeRecord(1.1, fuga);

        System.out.println(recordPattern(hoge));
    }

    private static String recordPattern(Object obj) {
        if (obj instanceof HogeRecord(double d, FugaRecord(int i, String s))) {
            return "d=" + d + ", i=" + i + ", s=" + s;
        } else {
            return "default";
        }
    }
}
実行結果
d=1.1, i=9, s=test
  • レコードパターンのパターンリストは、パターンに別のレコードを含めることでネストさせることができる
  • やりすぎると複雑になって可読性が落ちる気がする(私見)

テキストブロック

Hello World

public class TextBlockTest {
    public static void main(String[] args) {
        String text = """
                Hello
                World""";

        System.out.println("=======================");
        System.out.println(text);
        System.out.println("=======================");
    }
}
実行結果
=======================
Hello
World
=======================

説明

        String text = """
                Hello
                World""";
  • テキストブロックは、ダブルクォーテーション3つで文字列を囲うことで宣言できる
  • 最初のダブルクォーテーション3つの後は改行が入る必要がある
    • """Hello のようにダブルクォーテーション3つの直後から文字列を開始することはできない
  • テキストブロック内の文字列は、改行も含めそのままの形で String の値になる
    • 先頭の改行は含まれない
    • 改行コードは Line Feed (\n)になる
      • ソースコードの改行コードが \r\n になっていても、コンパイラによって \n に正規化される
    • インデントの扱いについては後述

エスケープ文字

public class TextBlockTest {
    public static void main(String[] args) {
        String text = """
                "double quotation"
                line\nfeed""";

        System.out.println("text = \"" + text + "\"");
    }
}
実行結果
=======================
"double quotation"
line
feed
=======================
  • 単一のダブルクォーテーションはエスケープ無しで記述できる
  • エスケープシーケンスは従来の文字列リテラルと同様に記述できる

末尾に改行を入れる

public class TextBlockTest {
    public static void main(String[] args) throws Exception {
        String text = """
                Hello
                World
                """;

        System.out.println("=======================");
        System.out.println(text);
        System.out.println("=======================");
    }
}
実行結果
=======================
Hello
World

=======================
  • 末尾に改行を入れたい場合は、終端のダブルクォーテーション3つを文字列の次の行に書けばいい

末尾の空白除去

public class TextBlockTest {
    public static void main(String[] args) throws Exception {
        String text = """
                half width white space***
                full width white space□□□
                tab->->->
                unicode Space\u0020\u0020\u0020
                unicode No-Break Space   
                unicode En Space\u2002\u2002\u2002
                unicode Thin Space\u2009\u2009\u2009
                unicode Hair Space\u200A\u200A\u200A
                unicode Idepgraphic Space\u3000\u3000\u3000""";

        System.out.println("=======================");
        System.out.println(text);
        System.out.println("=======================");
    }
}
  • 上記コードは、分かりやすくするために以下の文字を置換して記載している
    • 半角スペース -> *
    • 全角スペース   ->
    • タブ -> ->
実行結果
=======================
half width white space
full width white space
tab
unicode Space
unicode No-Break Space???
unicode En Space
unicode Thin Space
unicode Hair Space
unicode Idepgraphic Space
=======================
  • テキストブロック内の各行の末尾に存在するスペースは自動的に除去される
  • 全角やタブ、それにユニコードでスペース扱いになっているものもだいたい除去される(No-Break Space は除去されなかったので、全部ではなさそう)
    • まぁ、普段使うスペースは基本除去されるものと思っておいた方がよさそう
  • これをする理由は、テキストブロックの見た目と結果が常に一致するようにした方が間違いがなくて良いかららしい
    • 末尾のスペースを自動的に削除するようなエディタがあると、気づかぬうちに見た目と内容が変わるようなことがあるとかなんとか(ガイドにそんな感じのことが書かれていた)
    • This is done so that the contents of the text block are always visually discernible. If this were not done, a text editor that automatically strips trailing white space could invisibly change the contents of a text block.

    • https://docs.oracle.com/javase/jp/20/text-blocks/index.html#trailing-white-space
  • 末尾の空白を自動除去してほしくないときの方法がガイドに書かれているが、いずれもやや泥臭い方法になる
public class TextBlockTest {
    public static void main(String[] args) throws Exception {
        String s = """
                Hello$$$
                World""".replace("$", " ");

        String t = """
                Hello   |
                World""".replace("|\n", "\n");

        String u = """
                Hello  \040
                World""";

        String v = """
                Hello  \s
                World""";

        System.out.println("=======================");
        System.out.println(s);
        System.out.println("=======================");
        System.out.println(t);
        System.out.println("=======================");
        System.out.println(u);
        System.out.println("=======================");
    }
}
実行結果
=======================
Hello***
World
=======================
Hello***
World
=======================
Hello***
World
=======================
Hello***
World
=======================
  • 実行結果の方は、分かりやすくするために半角スペースを * に置換している
  • 末尾の空白を除去されたくない場合は、以下のいずれかの方法で対応できる
    • 文字列置換を使って後でスペースに置き換える
    • スペースの末尾に8進数表現のスペース(\040)かスペースのエスケープシーケンス (\s) を置く
      • これらはコンパイラによる末尾の空白の除外の後でスペースに置き換えられるので、除去を回避できるらしい
  • 個人的には、最後の「末尾に \s を置く」のが一番マシかなと思う(私見)

改行をエスケープする

public class TextBlockTest {
    public static void main(String[] args) throws Exception {
        String text = """
              Hello \
              World""";

        System.out.println("=======================");
        System.out.println(text);
        System.out.println("=======================");
    }
}
実行結果
=======================
Hello World
=======================
  • 各行の末尾の改行の手前に \ を置くことで、その改行をエスケープできる

インデント

public class TextBlockTest {
    public static void main(String[] args) {
        String text = """
****************Hoge
************
**************Fuga
******************Piyo""";

        System.out.println("=======================");
        System.out.println(text);
        System.out.println("=======================");
    }
}
実行結果
=======================
**Hoge

Fuga
****Piyo
=======================
  • 分かりやすくするために半角スペースを * に置換して表現している(実際はただの空白スペース)
  • テキストブロック内の各行の先頭のスペースは、余計な分が自動的に除去される
  • インデントが最も小さい行が基準となり、その行のインデントサイズが余計な分として判断される
    • Fuga の行が最もインデントが小さいので、その行のインデントが基準となり他の行の先頭のスペースも除去されている
    • 空行(空白だけの行も含む)はインデントの基準の判定からは除外されている
  • インデントが最も小さい行よりも手前の空白も残したい場合は、以下のようにする
public class TextBlockTest {
    public static void main(String[] args) throws Exception {
        String text = """
                Hoge
            
              Fuga
                  Piyo
            """;

        System.out.println("=======================");
        System.out.println(text);
        System.out.println("=======================");
    }
}
実行結果
=======================
    Hoge

  Fuga
      Piyo

=======================
  • 末尾のダブルクォーテーションを改行させ、先頭のスペースを維持したい高さに配置する
  • これにより、基準となるインデントの高さは末尾のダブルクォーテーションの位置となる
  • ただし、この場合は末尾に必ず改行コードが入ることになる
  • もし末尾のダブルクォーテーションでインデントを調整しつつ改行コードを最後に入れたくない場合は、次のようにする
    public static void main(String[] args) throws Exception {
        String text = """
                Hoge
            
              Fuga
                  Piyo\
            """;

        System.out.println("=======================");
        System.out.println(text);
        System.out.println("=======================");
    }
実行結果
=======================
    Hoge

  Fuga
      Piyo
=======================
  • 最後の改行の前に \ を入れることで、最後の改行をエスケープできる

参考

13
9
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
13
9