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
で型を定義する - クラス名の直後に、レコードに持たせるフィールドを宣言する
- この部分をレコードヘッダーと呼ぶ
- 上の実装例だと
(int number, String text)
の部分
- 上の実装例だと
- 個々のフィールドに対応する宣言をレコードコンポーネントと呼ぶ
- 上の実装例だと
int number
,String text
の部分
- 上の実装例だと
- 参考: 8.10. Record Classes | The Java Language Specification Java SE 21 Edition
- この部分をレコードヘッダーと呼ぶ
- 以上のコードで、以下のクラスを定義したのと同じになる
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 {
}
- レコードクラスは他のクラスを継承することはできない
レコードコンポーネントとアノテーション
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 {
}
-
Target
にFIELD
を設定したアノテーション
package java21.record;
...
@Target(ElementType.RECORD_COMPONENT)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRecordComponentAnnotation {
}
-
Target
にRECORD_COMPONENT
を設定したアノテーション
package java21.record;
...
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
}
-
Target
にMETHOD
を設定したアノテーション
package java21.record;
...
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyParameterAnnotation {
}
-
Target
にPARAMETER
を設定したアノテーション
package java21.record;
...
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTypeUseAnnotation {
}
-
Target
にTYPE_USE
を設定したアノテーション
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 値
という形で記述する
- その場合、 switch式の結果となる値は
- 入力された値が取りえる値を全てラベルとして列挙する必要があるため、基本的に
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
のように、default
をcase
のラベルに指定できるようになっている
列挙型を渡す場合
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 は「密封、密閉」といった意味)
Hello World
public sealed class MyClass permits Hoge, Fuga{
}
public final class Hoge extends MyClass {
}
public final class Fuga extends MyClass {
}
// '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
package java21.sealed;
// Class is not allowed to extend sealed class from another package
public sealed class MyClass permits Hoge, java21.sealed.sub.Foo {
}
package java21.sealed;
public final class Hoge extends MyClass {
}
package java21.sealed.sub;
import java21.sealed.MyClass;
public final class Foo extends MyClass {
}
- シールクラスが存在するプロジェクトには
module-info.java
がないので、無名モジュールとなっている - シールクラスである
MyClass
は、異なるパッケージに存在するFoo
をpermits
に指定している - しかし、名前付きモジュールでない場合は別パッケージのクラスを
permits
に指定できないので、コンパイルエラーとなっている - これのコンパイルを通すためには、シールクラスである
MyClass
が存在するプロジェクトを名前付きモジュールにする必要がある
src/main/java/
|-module-info.java ★追加
`-java21/sealed/
|-MyClass.java
|-Hoge.java
`-sub/
`-Foo.java
module test {
}
- これでコンパイルエラーが無くなる
sealed を連鎖させる
public sealed class MyClass permits Hoge {
}
public sealed class Hoge extends MyClass permits Fuga, Piyo {
}
public final class Fuga extends Hoge {
}
public final class Piyo extends Hoge {
}
-
permits
で継承を許可したクラスにsealed
を付けることで、さらにそのサブタイプを限定させることができる
任意のクラスが継承できるようにする
public sealed class MyClass permits Hoge, Fuga {
}
public final class Hoge extends MyClass {
}
public non-sealed class Fuga extends MyClass {
}
public class Foo extends Fuga {
}
-
permits
で継承を許可したクラスにnon-sealed
を付けると、そのクラスは任意のクラスで継承できるようになる
シールインタフェース
public sealed interface MyInterface permits Hoge, MyRecord {
}
public final class Hoge implements MyInterface {
}
public record MyRecord() implements MyInterface {
}
// '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() < 10
はs 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() < 10
はobj 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 条件式
のように記述することで、型以外の任意の条件を追加できる - 上の実装の場合、
obj
がString
でかつ長さが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つ目の条件は常に除外されるようになるため、コンパイルエラーとなっている
シールクラスのパターンマッチ
public sealed class MySealedClass permits Hoge, Fuga {
}
public final class Hoge extends MySealedClass {
}
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 が来た場合が網羅されていないのでコンパイルエラー
};
}
private static String patternMatch(MySealedClass msc) {
return switch (msc) {
// Hoge, Fuga が来た場合も MySealedClass として処理されるので網羅できている
case MySealedClass m -> "MySealedClass";
};
}
non-sealed が含まれる場合
public sealed class MySealedClass permits Hoge, Fuga {
}
public final class Hoge extends MySealedClass {
}
public non-sealed class Fuga extends MySealedClass {
}
public class Piyo extends Fuga {
}
-
Fuga
をnon-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
-
Piyo
はFuga
のサブタイプなので、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
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
を使うのは危険だと思う - しかし、ここはレコードクラスのコンポーネントであることがハッキリしているので、型は人間にも推測できると思う(私見)
- なんの型の値が入っているかぱっと見で分からない場所で
レコードパターンのネスト
public record HogeRecord(double d, FugaRecord 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
=======================
- 最後の改行の前に
\
を入れることで、最後の改行をエスケープできる