Share
Embed
シールドクラスをはじめよう / Getting Started with Sealed Classes
2024-10-27 に開催された JJUG CCC 2024 Fall での登壇資料です。
https://jjug.doorkeeper.jp/events/177443
mackey0225
October 27, 2024
Transcript
73 slides
-
シールドクラスを はじめよう 2024-10-27 JJUG CCC 2024 Fall BABY JOB 株式会社
浅野 正貴 (@mackey0225) -
1: Java 7 以前 2: Java 8 3: Java 11
4: Java 17 5: Java 21 6: Java 22 以降 -
1: Java 7 以前 2: Java 8 3: Java 11
4: Java 17 5: Java 21 6: Java 22 以降 今日の話は Java 17 の追加機能です! -
1: Java 7 以前 2: Java 8 3: Java 11
4: Java 17 5: Java 21 6: Java 22 以降 この話で Ver.UP の モチベ上げられたら嬉しい -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 自己紹介 名前:浅野 正貴 所属:BABY JOB 株式会社 最近はインフラや SRE
がメイン X: @mackey0225 タイトルは最近の O'Reilly の書籍より拝借 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 自己紹介 名前:浅野 正貴 所属:BABY JOB 株式会社 最近はインフラや SRE
がメイン X: @mackey0225 タイトルは最近の O'Reilly の書籍より拝借 JJUG CCC 2024 Fall ロゴスポンサーです!! -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 諸注意 • 本セッションは Step UP なので基礎重視 •
アクセス修飾子や static 修飾子は割愛 • コンストラクタやアクセッサも割愛 • 呼び方は「シール・クラス」が一般的 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 諸注意 • 本セッションは Step UP なので基礎重視 •
アクセス修飾子や static 修飾子は割愛 • コンストラクタやアクセッサも割愛 • 呼び方は「シール・クラス」が一般的 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i どんな機能なの? • sealed という名の通り、蓋をする ◦ 継承・実装の関係を制限 •
宣言的な定義により、以下を実現 ◦ コンパイルフェーズでの整合性確保→保守性の向上 • リフレクション(黒魔術)も追加(今日は割愛) • Project Amber のいち機能 ◦ Java 15 でプレビュー、Java 17 で正式追加 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i もう少し踏み込んで:Project Amber について • 生産性向上を目的とした機能の提供 • 他の機能例
◦ ローカル変数の var の導入 ◦ レコードクラス ◦ switch のパターンマッチ ◦ パブリックスタティックヴォイドメイン からの解放(現在 Preview) -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i シールドクラスにおける登場人物 • sealed 修飾子 / permits 句
• non-sealed 修飾子 • final 修飾子 • 修飾子なし -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 基本的な使い方(イメージ) sealed class 親 permits 長子, 中間子,
末子 {} sealed class 長子 extends 親 permits 孫 {} final class 中間子 extends 親 {} non-sealed class 末子 extends 親 {} final class 孫 extends 長子 {} -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i sealed 修飾子 / permits • Java 17
で正式追加 • 継承・実装する対象を制限する際に宣言 • permits 句内で許可対象を指定する • 継承・実装側はさらに制限の指定が必要 ◦ sealed, non-sealed, final のいずれかを指定が必要 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i permits 句にない対象は継承・実装できない sealed class 親 permits 子
{} final class 子 extends 親 {} // こっちは OK final class 他人 extends 親 {} // こっちは赤の他人なので NG -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i permits 句にない対象は継承・実装できない sealed class 親 permits 子
{} final class 子 extends 親 {} // こっちは OK final class 他人 extends 親 {} // こっちは赤の他人なので NG -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i non-sealed 修飾子 • sealed と同じく Java 17
から正式追加 • sealed とは逆に継承や実装に制限をかけない ◦ 以降は自由に継承・実装していいよ • 元側で sealed がないのに宣言するとコンパイルエラー ◦ 要は「何、勝手なこと言っているんだ?」状態 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i non-sealed 単独ではコンパイルエラー class Foo {} non-sealed class
Bar extends Foo {} // 急な non-sealed はダメ -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i non-sealed 単独ではコンパイルエラー class Foo {} non-sealed class
Bar extends Foo {} // 急な non-sealed はダメ -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i final 修飾子 • 当初から存在 • final がついたクラスを継承することはできない
• インターフェースには使えない(コンパイルエラー) ◦ 実装前提だから、それはそう • abstract とは併用できない(コンパイルエラー) ◦ 継承前提だから、それはそう -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 修飾子なし • 当初から存在 ◦ というか、無いのがある意味普通 • シールドクラスを継承時、修飾子なしはコンパイルエラー
◦ インターフェースでも同様 ◦ ただし、暗黙的に final のものは問題なし ▪ 例: record や enum など -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i スーパー側が sealed 設定時の挙動 sealed class Foo permits
Bar {} class Bar extends Foo {} // 継承元の sealed 修飾子を無視できない -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i スーパー側が sealed 設定時の挙動 sealed class Foo permits
Bar {} class Bar extends Foo {} // 継承元の sealed 修飾子を無視できない -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i スーパー側が sealed 設定時の挙動 sealed class Foo permits
Bar {} non-sealed class Bar extends Foo {} class Baz extends Bar {} // これは OK -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i スーパー側が sealed 設定時の挙動 sealed class Foo permits
Bar {} non-sealed class Bar extends Foo {} class Baz extends Bar {} // これは OK -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i スーパー側が sealed 設定時の挙動 sealed interface Foo permits
Bar {} record Bar(int id) implements Foo {} // これは OK (暗黙的 final) -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i スーパー側が sealed 設定時の挙動 sealed interface Foo permits
Bar {} record Bar(int id) implements Foo {} // これは OK (暗黙的 final) -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i switch のパターンマッチ • Java 21 から以下が追加されている ◦
JEP 441: Pattern Matching for switch ◦ https://openjdk.org/jeps/441 • case ラベルに型を記述して条件分岐できる ◦ 具体的な仕様はこのあと説明 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 不使用】 interface 決済 {} class 現金決済
implements 決済 {} class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 不使用】 interface 決済 {} class 現金決済
implements 決済 {} class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } このままでもいけそうな気がするけど、 default が必要 → コンパイルエラー -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 不使用】 interface 決済 {} class 現金決済
implements 決済 {} class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); default -> System.out.println("デフォルト "); } -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 不使用】 interface 決済 {} class 現金決済
implements 決済 {} class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); default -> System.out.println("デフォルト "); } では、sealed を 使うとどうなるか? -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 使用】 sealed interface 決済 permits 現金決済,
クレカ決済 {} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 使用】 sealed interface 決済 permits 現金決済,
クレカ決済 {} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } 実装するクラスが明示して列挙されているので default が不要になった!\(^o^)/ -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 使用】 sealed interface 決済 permits 現金決済,
クレカ決済 {} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } 実装するクラスが明示して列挙されているので default が不要になった!\(^o^)/ え、メリットって それだけ? -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i パターンマッチにおける網羅性【sealed 使用】 sealed interface 決済 permits 現金決済,
クレカ決済 {} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } 実装するクラスが明示して列挙されているので default が不要になった!\(^o^)/ いやいや それだけじゃないんよ -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 新たに実装が増えたとき sealed interface 決済 permits 現金決済, クレカ決済
{} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 新たに実装が増えたとき sealed interface 決済 permits 現金決済, クレカ決済
, QR決済 {} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} final class QR決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 新たに実装が増えたとき sealed interface 決済 permits 現金決済, クレカ決済
, QR決済 {} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} final class QR決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } コンパイルフェーズで QR決済ラベルが足りない指摘をしてくれる -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 新たに実装が増えたとき sealed interface 決済 permits 現金決済, クレカ決済
, QR決済 {} final class 現金決済 implements 決済 {} final class クレカ決済 implements 決済 {} final class QR決済 implements 決済 {} switch (k) { case 現金決済 g -> System.out.println("現金"); case クレカ決済 c -> System.out.println("クレカ"); } コンパイルフェーズで QR決済ラベルが足りない指摘をしてくれる 実装漏れや考慮漏れを防げる -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 代数的データ型 代数的データ型(Algebraic Data Types)とは... • 集合の 直和
と 直積 で型を表現する ◦ 直和:enum クラス(どれか一つをとる) ◦ 直積:record クラス(カラムの掛け合わ せ) -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 代数的データ型 代数的データ型(Algebraic Data Types)とは... • 集合の 直和
と 直積 で型を表現する ◦ 直和:enum クラス(どれか一つをとる) ◦ 直積:record クラス(カラムの掛け合わ せ) とりあえず、雰囲気だけでOK! -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i • 契約ID 例:契約状態の表現 未契約 • 契約ID •
開始日 • 契約ID • 開始日 • 終了日 契約中 解約 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i • 契約ID 例:契約状態の表現 未契約 • 契約ID •
開始日 • 契約ID • 開始日 • 終了日 契約中 解約 状態で必要な項目(開始日と終了日)の有無が異なる -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型でない) enum 契約状態 {未契約,契約中,解約;} class 契約 {
int id; LocalDate 開始日; LocalDate 終了日; 契約状態 契約状態; } -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型でない) enum 契約状態 {未契約,契約中,解約;} class 契約 {
int id; LocalDate 開始日; LocalDate 終了日; 契約状態 契約状態; } // 未契約の生成 new 契約(1, null, null, 契約状態.未契約); // 契約中の生成 new 契約(2, 開始日, null, 契約状態.契約中); // 解約の生成 new 契約(3, 開始日, 終了日, 契約状態.解約); -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型でない) enum 契約状態 {未契約,契約中,解約;} class 契約 {
int id; LocalDate 開始日; LocalDate 終了日; 契約状態 契約状態; } // 未契約の生成 new 契約(1, null, null, 契約状態.未契約); // 契約中の生成 new 契約(2, 開始日, null, 契約状態.契約中); // 解約の生成 new 契約(3, 開始日, 終了日, 契約状態.解約); 状態に応じて、不要な日付項目に null いれる -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型でない) enum 契約状態 {未契約,契約中,解約;} class 契約 {
int id; LocalDate 開始日; LocalDate 終了日; 契約状態 契約状態; } // 未契約の生成 new 契約(1, null, null, 契約状態.未契約); // 契約中の生成 new 契約(2, 開始日, null, 契約状態.契約中); // 解約の生成 new 契約(3, 開始日, 終了日, 契約状態.解約); 状態に応じて、不要な日付項目に null いれる 整合性をロジック・コードで担保する 💪 -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型でない) enum 契約状態 {未契約,契約中,解約;} class 契約 {
int id; LocalDate 開始日; LocalDate 終了日; 契約状態 契約状態; } // 未契約の生成 new 契約(1, null, null, 契約状態.未契約); // 契約中の生成 new 契約(2, 開始日, null, 契約状態.契約中); // 解約の生成 new 契約(3, 開始日, 終了日, 契約状態.解約); 状態に応じて、不要な日付項目に null いれる 整合性をロジック・コードで担保する 💪 バリデーションや複雑なファクトリが増える... ☹ -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型でない) enum 契約状態 {未契約,契約中,解約;} class 契約 {
int id; LocalDate 開始日; LocalDate 終了日; 契約状態 契約状態; } // 未契約の生成 new 契約(1, null, null, 契約状態.未契約); // 契約中の生成 new 契約(2, 開始日, null, 契約状態.契約中); // 解約の生成 new 契約(3, 開始日, 終了日, 契約状態.解約); 状態に応じて、不要な日付項目に null いれる 整合性をロジック・コードで担保する 💪 バリデーションや複雑なファクトリが増える... ☹ そこに時間や労力を 掛けるのはもったいないかも -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型) sealed interface 契約 permits 未契約, 契約中,
解約 { int id(); } record 未契約(int id) implements 契約 {} record 契約中(int id, LocalDate 開始日) implements 契約 {} record 解約(int id, LocalDate 開始日, LocalDate 終了日) implements 契約 {} -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型) sealed interface 契約 permits 未契約, 契約中,
解約 { int id(); } record 未契約(int id) implements 契約 {} record 契約中(int id, LocalDate 開始日) implements 契約 {} record 解約(int id, LocalDate 開始日, LocalDate 終了日) implements 契約 {} // 未契約の生成 new 未契約(1); // 契約中の生成 new 契約中(2, 開始日); // 解約の生成 new 解約(3, 開始日, 終了日); -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型) sealed interface 契約 permits 未契約, 契約中,
解約 { int id(); } record 未契約(int id) implements 契約 {} record 契約中(int id, LocalDate 開始日) implements 契約 {} record 解約(int id, LocalDate 開始日, LocalDate 終了日) implements 契約 {} // 未契約の生成 new 未契約(1); // 契約中の生成 new 契約中(2, 開始日); // 解約の生成 new 解約(3, 開始日, 終了日); 不要な項目は設定できない -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型) sealed interface 契約 permits 未契約, 契約中,
解約 { int id(); } record 未契約(int id) implements 契約 {} record 契約中(int id, LocalDate 開始日) implements 契約 {} record 解約(int id, LocalDate 開始日, LocalDate 終了日) implements 契約 {} // 未契約の生成 new 未契約(1); // 契約中の生成 new 契約中(2, 開始日); // 解約の生成 new 解約(3, 開始日, 終了日); 不要な項目は設定できない 型で整合性を担保し、型で表現する -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 例:契約状態の表現(代数的データ型) sealed interface 契約 permits 未契約, 契約中,
解約 { int id(); } record 未契約(int id) implements 契約 {} record 契約中(int id, LocalDate 開始日) implements 契約 {} record 解約(int id, LocalDate 開始日, LocalDate 終了日) implements 契約 {} // 未契約の生成 new 未契約(1); // 契約中の生成 new 契約中(2, 開始日); // 解約の生成 new 解約(3, 開始日, 終了日); 不要な項目は設定できない 型で整合性を担保し、型で表現する コンパイル時に気づくことができる さらに先のパターンマッチとの併用でより効果的に扱える -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i まとめ • コンパイルフェーズで防げるのは魅力 • その他の機能と組み合わせることで型の表現力が上がる • とはいえ、チームの状況に合わせてね
◦ 用法用量を守ることが大事 • 大前提が Java 17 ◦ 定期的なバージョンアップも必要 ◦ Java はドンドン進化する -
シールドクラスをはじめよう #jjug_ccc #jjug_ccc_i 参考 [Sealed Classes] https://openjdk.org/jeps/409 [Pattern Matching for
switch] https://openjdk.org/jeps/441