From ff6130bede140d818a0f9f963c75921d385afa2e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 12:03:59 +0000 Subject: [PATCH] Add sealed module: modelling domains with sealed types and exhaustive switch Payment model, permits and the three modifiers, default-branch trap, generic and recursive hierarchies, visitor comparison, run-time enforcement, modules. Transcripts from JDK 21, 25 and 27. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a --- README.md | 1 + sealed/README.md | 15 +++ sealed/broken/AnonymousSealed.java | 7 ++ sealed/broken/MissingModifier.java | 4 + sealed/broken/NoSubclass.java | 3 + sealed/broken/NonSealedWrongPlace.java | 4 + sealed/broken/NotPermitted.java | 6 + sealed/broken/VisitorAdded.java | 17 +++ sealed/broken/WalletNotHandled.java | 16 +++ sealed/broken/xpkg/a/Base.java | 2 + sealed/broken/xpkg/b/Impl.java | 2 + sealed/multi/demo.pay/module-info.java | 1 + sealed/multi/demo.pay/pay/api/Payment.java | 2 + sealed/multi/demo.pay/pay/app/Main.java | 10 ++ sealed/multi/demo.pay/pay/impl/Card.java | 2 + sealed/multi/demo.pay/pay/impl/Upi.java | 2 + sealed/output/01-payment-demo.txt | 17 +++ sealed/output/02-permits-and-modifiers.txt | 44 +++++++ sealed/output/03-compile-errors.txt | 130 +++++++++++++++++++++ sealed/output/04-default-trap.txt | 8 ++ sealed/output/05-generic-and-recursive.txt | 26 +++++ sealed/output/06-visitor.txt | 41 +++++++ sealed/output/07-runtime-enforcement.txt | 14 +++ sealed/output/08-modules.txt | 8 ++ sealed/run.sh | 59 ++++++++++ sealed/sep/Main.java | 7 ++ sealed/sep/evil/Card.java | 1 + sealed/sep/evil/Hack.java | 1 + sealed/sep/evil/Payment.java | 1 + sealed/sep/v1/Card.java | 1 + sealed/sep/v1/Payment.java | 1 + sealed/src/ExprDemo.java | 37 ++++++ sealed/src/ImplicitPermits.java | 10 ++ sealed/src/PaymentDemo.java | 35 ++++++ sealed/src/Reflect.java | 33 ++++++ sealed/src/ResultDemo.java | 31 +++++ sealed/src/VisitorVersion.java | 19 +++ sealed/src/WithDefault.java | 20 ++++ 38 files changed, 638 insertions(+) create mode 100644 sealed/README.md create mode 100644 sealed/broken/AnonymousSealed.java create mode 100644 sealed/broken/MissingModifier.java create mode 100644 sealed/broken/NoSubclass.java create mode 100644 sealed/broken/NonSealedWrongPlace.java create mode 100644 sealed/broken/NotPermitted.java create mode 100644 sealed/broken/VisitorAdded.java create mode 100644 sealed/broken/WalletNotHandled.java create mode 100644 sealed/broken/xpkg/a/Base.java create mode 100644 sealed/broken/xpkg/b/Impl.java create mode 100644 sealed/multi/demo.pay/module-info.java create mode 100644 sealed/multi/demo.pay/pay/api/Payment.java create mode 100644 sealed/multi/demo.pay/pay/app/Main.java create mode 100644 sealed/multi/demo.pay/pay/impl/Card.java create mode 100644 sealed/multi/demo.pay/pay/impl/Upi.java create mode 100644 sealed/output/01-payment-demo.txt create mode 100644 sealed/output/02-permits-and-modifiers.txt create mode 100644 sealed/output/03-compile-errors.txt create mode 100644 sealed/output/04-default-trap.txt create mode 100644 sealed/output/05-generic-and-recursive.txt create mode 100644 sealed/output/06-visitor.txt create mode 100644 sealed/output/07-runtime-enforcement.txt create mode 100644 sealed/output/08-modules.txt create mode 100755 sealed/run.sh create mode 100644 sealed/sep/Main.java create mode 100644 sealed/sep/evil/Card.java create mode 100644 sealed/sep/evil/Hack.java create mode 100644 sealed/sep/evil/Payment.java create mode 100644 sealed/sep/v1/Card.java create mode 100644 sealed/sep/v1/Payment.java create mode 100644 sealed/src/ExprDemo.java create mode 100644 sealed/src/ImplicitPermits.java create mode 100644 sealed/src/PaymentDemo.java create mode 100644 sealed/src/Reflect.java create mode 100644 sealed/src/ResultDemo.java create mode 100644 sealed/src/VisitorVersion.java create mode 100644 sealed/src/WithDefault.java diff --git a/README.md b/README.md index d1c1cf9..b10bba0 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T | [`jep513/`](jep513) | Flexible Constructor Bodies in Java 25 (JEP 513): Validate Before super() | statements before `super(...)`/`this(...)`, the parent-calls-child bug and its fix, twelve compile errors that remain with the 25 and 27 wordings side by side, `--release` rejection | | [`jep511/`](jep511) | Module Import Declarations in Java 25 (JEP 511) | `import module java.base;` against seven single imports, ambiguity errors and how single-type, on-demand and same-package names beat a module import, what one module import does and does not bring in (`java.sql` does not give `java.base`; `java.se` needs `--add-modules`), named modules, JEP 512 compact files, jshell's default imports, identical bytecode | | [`patterns/`](patterns) | Pattern Matching in Java: switch, Record Patterns and Primitive Patterns (JDK 21 to 27) | type and record patterns, guards and dominance errors, sealed exhaustiveness (with JDK 27's new `missing patterns` lines), `MatchException` from a throwing accessor and from separate compilation, unnamed patterns, primitive patterns with `--enable-preview` and the preview class-file trap | +| [`sealed/`](sealed) | Sealed Classes and Interfaces: Modelling Domains with Exhaustive switch | a `Payment` model, `permits` and the three subclass modifiers, the `default` trap, generic and recursive hierarchies, the visitor for comparison, run-time enforcement and modules | ## Captured output (`docs/output/`) diff --git a/sealed/README.md b/sealed/README.md new file mode 100644 index 0000000..c582ec7 --- /dev/null +++ b/sealed/README.md @@ -0,0 +1,15 @@ +# sealed — Sealed classes and interfaces + +Companion code for the ankurm.com article **Sealed Classes and Interfaces: Modelling Domains with Exhaustive switch**. The explanation lives in the +article; this folder holds the runnable sources and the transcripts they produced. + +```bash +JDK25=/path/to/jdk-25 JDK27=/path/to/jdk-27 [JDK21=/path/to/jdk-21] ./run.sh # regenerates output/*.txt +``` + +- `src/` compiles and runs unchanged on 21, 25 and 27: the `Payment` model, the three subclass modifiers, generic and recursive hierarchies, the `default` trap and a visitor for comparison. +- `broken/` fails to compile on purpose (one rule per file). +- `sep/` shows what the JVM itself enforces when a class file disagrees with the sealed type it claims to extend. +- `multi/` is a named module, the only place a sealed hierarchy may span packages. + +Tested on Temurin 25.0.4.1+1 and 27+35, and OpenJDK 21.0.10 as the baseline. diff --git a/sealed/broken/AnonymousSealed.java b/sealed/broken/AnonymousSealed.java new file mode 100644 index 0000000..c657518 --- /dev/null +++ b/sealed/broken/AnonymousSealed.java @@ -0,0 +1,7 @@ +public class AnonymousSealed { + sealed interface Payment permits Card {} + record Card(long amount) implements Payment {} + static Payment sneaky() { + return new Payment() {}; // an anonymous class is a subtype nobody listed + } +} diff --git a/sealed/broken/MissingModifier.java b/sealed/broken/MissingModifier.java new file mode 100644 index 0000000..abddb44 --- /dev/null +++ b/sealed/broken/MissingModifier.java @@ -0,0 +1,4 @@ +public class MissingModifier { + sealed interface Payment permits Card {} + class Card implements Payment {} // must say final, sealed or non-sealed +} diff --git a/sealed/broken/NoSubclass.java b/sealed/broken/NoSubclass.java new file mode 100644 index 0000000..8589718 --- /dev/null +++ b/sealed/broken/NoSubclass.java @@ -0,0 +1,3 @@ +public class NoSubclass { + sealed interface Payment {} // no permits clause and no subtype in this file +} diff --git a/sealed/broken/NonSealedWrongPlace.java b/sealed/broken/NonSealedWrongPlace.java new file mode 100644 index 0000000..f1b345f --- /dev/null +++ b/sealed/broken/NonSealedWrongPlace.java @@ -0,0 +1,4 @@ +public class NonSealedWrongPlace { + interface Plain {} + non-sealed class Oops implements Plain {} // non-sealed only makes sense under a sealed supertype +} diff --git a/sealed/broken/NotPermitted.java b/sealed/broken/NotPermitted.java new file mode 100644 index 0000000..be69d3b --- /dev/null +++ b/sealed/broken/NotPermitted.java @@ -0,0 +1,6 @@ +public class NotPermitted { + sealed interface Payment permits Card, Upi {} + record Card(long amount) implements Payment {} + record Upi(long amount) implements Payment {} + record Cheque(long amount) implements Payment {} // not in the permits clause +} diff --git a/sealed/broken/VisitorAdded.java b/sealed/broken/VisitorAdded.java new file mode 100644 index 0000000..c737795 --- /dev/null +++ b/sealed/broken/VisitorAdded.java @@ -0,0 +1,17 @@ +public class VisitorAdded { + interface Payment { R accept(Visitor v); } + interface Visitor { R visitCard(Card c); R visitUpi(Upi u); R visitWallet(Wallet w); } // Wallet added here + record Card() implements Payment { public R accept(Visitor v) { return v.visitCard(this); } } + record Upi() implements Payment { public R accept(Visitor v) { return v.visitUpi(this); } } + record Wallet() implements Payment { public R accept(Visitor v) { return v.visitWallet(this); } } + + // Two existing operations, neither knows about Wallet: each one now fails to compile. + static class Fee implements Visitor { + public Long visitCard(Card c) { return 2L; } + public Long visitUpi(Upi u) { return 0L; } + } + static class Label implements Visitor { + public String visitCard(Card c) { return "card"; } + public String visitUpi(Upi u) { return "upi"; } + } +} diff --git a/sealed/broken/WalletNotHandled.java b/sealed/broken/WalletNotHandled.java new file mode 100644 index 0000000..c688c83 --- /dev/null +++ b/sealed/broken/WalletNotHandled.java @@ -0,0 +1,16 @@ +public class WalletNotHandled { + sealed interface Payment permits Card, Upi, NetBanking, Wallet {} // Wallet added + record Card(long amount) implements Payment {} + record Upi(long amount) implements Payment {} + record NetBanking(long amount) implements Payment {} + record Wallet(long amount) implements Payment {} + + // Same switch as before, with no default and no case for Wallet. + static long fee(Payment p) { + return switch (p) { + case Card c -> c.amount() * 2 / 100; + case Upi u -> 0; + case NetBanking n -> 10; + }; + } +} diff --git a/sealed/broken/xpkg/a/Base.java b/sealed/broken/xpkg/a/Base.java new file mode 100644 index 0000000..3cd77c6 --- /dev/null +++ b/sealed/broken/xpkg/a/Base.java @@ -0,0 +1,2 @@ +package a; +public sealed interface Base permits b.Impl {} diff --git a/sealed/broken/xpkg/b/Impl.java b/sealed/broken/xpkg/b/Impl.java new file mode 100644 index 0000000..876563d --- /dev/null +++ b/sealed/broken/xpkg/b/Impl.java @@ -0,0 +1,2 @@ +package b; +public final class Impl implements a.Base {} diff --git a/sealed/multi/demo.pay/module-info.java b/sealed/multi/demo.pay/module-info.java new file mode 100644 index 0000000..d718939 --- /dev/null +++ b/sealed/multi/demo.pay/module-info.java @@ -0,0 +1 @@ +module demo.pay { } diff --git a/sealed/multi/demo.pay/pay/api/Payment.java b/sealed/multi/demo.pay/pay/api/Payment.java new file mode 100644 index 0000000..ef50647 --- /dev/null +++ b/sealed/multi/demo.pay/pay/api/Payment.java @@ -0,0 +1,2 @@ +package pay.api; +public sealed interface Payment permits pay.impl.Card, pay.impl.Upi {} diff --git a/sealed/multi/demo.pay/pay/app/Main.java b/sealed/multi/demo.pay/pay/app/Main.java new file mode 100644 index 0000000..d8db687 --- /dev/null +++ b/sealed/multi/demo.pay/pay/app/Main.java @@ -0,0 +1,10 @@ +package pay.app; +import pay.api.Payment; +import pay.impl.*; +public class Main { + public static void main(String[] args) { + Payment p = new Card(100); + String s = switch (p) { case Card c -> "card"; case Upi u -> "upi"; }; + System.out.println(s + " in module " + Payment.class.getModule().getName()); + } +} diff --git a/sealed/multi/demo.pay/pay/impl/Card.java b/sealed/multi/demo.pay/pay/impl/Card.java new file mode 100644 index 0000000..73b0abf --- /dev/null +++ b/sealed/multi/demo.pay/pay/impl/Card.java @@ -0,0 +1,2 @@ +package pay.impl; +public record Card(long amount) implements pay.api.Payment {} diff --git a/sealed/multi/demo.pay/pay/impl/Upi.java b/sealed/multi/demo.pay/pay/impl/Upi.java new file mode 100644 index 0000000..42365e2 --- /dev/null +++ b/sealed/multi/demo.pay/pay/impl/Upi.java @@ -0,0 +1,2 @@ +package pay.impl; +public record Upi(long amount) implements pay.api.Payment {} diff --git a/sealed/output/01-payment-demo.txt b/sealed/output/01-payment-demo.txt new file mode 100644 index 0000000..7bb5287 --- /dev/null +++ b/sealed/output/01-payment-demo.txt @@ -0,0 +1,17 @@ +$ javac src/PaymentDemo.java && java PaymentDemo (21.0.10+7-Ubuntu-124.04) +Card[last4=4242, amount=500] -> fee 10 | card payment ending 4242 +Card[last4=1111, amount=25000] -> fee 500 | large card payment ending 1111 +Upi[vpa=ankur@bank, amount=500] -> fee 0 | UPI payment from ankur@bank +NetBanking[bank=HDFC, amount=500] -> fee 15 | net banking via HDFC + +$ javac src/PaymentDemo.java && java PaymentDemo (25.0.4.1+1-LTS) +Card[last4=4242, amount=500] -> fee 10 | card payment ending 4242 +Card[last4=1111, amount=25000] -> fee 500 | large card payment ending 1111 +Upi[vpa=ankur@bank, amount=500] -> fee 0 | UPI payment from ankur@bank +NetBanking[bank=HDFC, amount=500] -> fee 15 | net banking via HDFC + +$ javac src/PaymentDemo.java && java PaymentDemo (27+35) +Card[last4=4242, amount=500] -> fee 10 | card payment ending 4242 +Card[last4=1111, amount=25000] -> fee 500 | large card payment ending 1111 +Upi[vpa=ankur@bank, amount=500] -> fee 0 | UPI payment from ankur@bank +NetBanking[bank=HDFC, amount=500] -> fee 15 | net banking via HDFC diff --git a/sealed/output/02-permits-and-modifiers.txt b/sealed/output/02-permits-and-modifiers.txt new file mode 100644 index 0000000..7353a4c --- /dev/null +++ b/sealed/output/02-permits-and-modifiers.txt @@ -0,0 +1,44 @@ +$ javac src/Reflect.java && java Reflect (21.0.10+7-Ubuntu-124.04) +Shape: sealed, permits [Circle, Poly, Custom] +Circle: final +Poly: sealed, permits [Tri, Quad] +Tri: final +Quad: final +Custom: open (non-sealed or plain) +Hexagon: open (non-sealed or plain) +circle triangle quad custom:Hexagon + +$ javac src/Reflect.java && java Reflect (25.0.4.1+1-LTS) +Shape: sealed, permits [Circle, Poly, Custom] +Circle: final +Poly: sealed, permits [Tri, Quad] +Tri: final +Quad: final +Custom: open (non-sealed or plain) +Hexagon: open (non-sealed or plain) +circle triangle quad custom:Hexagon + +$ javac src/Reflect.java && java Reflect (27+35) +Shape: sealed, permits [Circle, Poly, Custom] +Circle: final +Poly: sealed, permits [Tri, Quad] +Tri: final +Quad: final +Custom: open (non-sealed or plain) +Hexagon: open (non-sealed or plain) +circle triangle quad custom:Hexagon + +$ javac src/ImplicitPermits.java && java ImplicitPermits (21.0.10+7-Ubuntu-124.04) +[ImplicitPermits$A, ImplicitPermits$B] + +$ javac src/ImplicitPermits.java && java ImplicitPermits (25.0.4.1+1-LTS) +[ImplicitPermits$A, ImplicitPermits$B] + +$ javac src/ImplicitPermits.java && java ImplicitPermits (27+35) +[ImplicitPermits$A, ImplicitPermits$B] + +$ javap -v -cp . 'PaymentDemo$Payment' | grep -A3 PermittedSubclasses (25.0.4.1+1-LTS) +PermittedSubclasses: + PaymentDemo$Card + PaymentDemo$Upi + PaymentDemo$NetBanking diff --git a/sealed/output/03-compile-errors.txt b/sealed/output/03-compile-errors.txt new file mode 100644 index 0000000..f98951d --- /dev/null +++ b/sealed/output/03-compile-errors.txt @@ -0,0 +1,130 @@ +$ javac broken/WalletNotHandled.java (21.0.10+7-Ubuntu-124.04) +broken/WalletNotHandled.java:10: error: the switch expression does not cover all possible input values + return switch (p) { + ^ +1 error + +$ javac broken/WalletNotHandled.java (25.0.4.1+1-LTS) +broken/WalletNotHandled.java:10: error: the switch expression does not cover all possible input values + return switch (p) { + ^ +1 error + +$ javac broken/WalletNotHandled.java (27+35) +broken/WalletNotHandled.java:10: error: the switch expression does not cover all possible input values + return switch (p) { + ^ + missing patterns: + Wallet _ +1 error + +$ javac broken/NotPermitted.java (21.0.10+7-Ubuntu-124.04) +broken/NotPermitted.java:5: error: class is not allowed to extend sealed class: Payment (as it is not listed in its 'permits' clause) + record Cheque(long amount) implements Payment {} // not in the permits clause + ^ +1 error + +$ javac broken/NotPermitted.java (25.0.4.1+1-LTS) +broken/NotPermitted.java:5: error: class is not allowed to extend sealed class: Payment (as it is not listed in its 'permits' clause) + record Cheque(long amount) implements Payment {} // not in the permits clause + ^ +1 error + +$ javac broken/NotPermitted.java (27+35) +broken/NotPermitted.java:5: error: class is not allowed to extend sealed class: Payment (as it is not listed in its 'permits' clause) + record Cheque(long amount) implements Payment {} // not in the permits clause + ^ +1 error + +$ javac broken/MissingModifier.java (21.0.10+7-Ubuntu-124.04) +broken/MissingModifier.java:3: error: sealed, non-sealed or final modifiers expected + class Card implements Payment {} // must say final, sealed or non-sealed + ^ +1 error + +$ javac broken/MissingModifier.java (25.0.4.1+1-LTS) +broken/MissingModifier.java:3: error: sealed, non-sealed or final modifiers expected + class Card implements Payment {} // must say final, sealed or non-sealed + ^ +1 error + +$ javac broken/MissingModifier.java (27+35) +broken/MissingModifier.java:3: error: sealed, non-sealed or final modifiers expected + class Card implements Payment {} // must say final, sealed or non-sealed + ^ +1 error + +$ javac broken/NoSubclass.java (21.0.10+7-Ubuntu-124.04) +broken/NoSubclass.java:2: error: sealed class must have subclasses + sealed interface Payment {} // no permits clause and no subtype in this file + ^ +1 error + +$ javac broken/NoSubclass.java (25.0.4.1+1-LTS) +broken/NoSubclass.java:2: error: sealed class must have subclasses + sealed interface Payment {} // no permits clause and no subtype in this file + ^ +1 error + +$ javac broken/NoSubclass.java (27+35) +broken/NoSubclass.java:2: error: sealed class must have subclasses + sealed interface Payment {} // no permits clause and no subtype in this file + ^ +1 error + +$ javac broken/NonSealedWrongPlace.java (21.0.10+7-Ubuntu-124.04) +broken/NonSealedWrongPlace.java:3: error: non-sealed modifier not allowed here + non-sealed class Oops implements Plain {} // non-sealed only makes sense under a sealed supertype + ^ + (class NonSealedWrongPlace.Oops does not have any sealed supertypes) +1 error + +$ javac broken/NonSealedWrongPlace.java (25.0.4.1+1-LTS) +broken/NonSealedWrongPlace.java:3: error: non-sealed modifier not allowed here + non-sealed class Oops implements Plain {} // non-sealed only makes sense under a sealed supertype + ^ + (class NonSealedWrongPlace.Oops does not have any sealed supertypes) +1 error + +$ javac broken/NonSealedWrongPlace.java (27+35) +broken/NonSealedWrongPlace.java:3: error: non-sealed modifier not allowed here + non-sealed class Oops implements Plain {} // non-sealed only makes sense under a sealed supertype + ^ + (class NonSealedWrongPlace.Oops does not have any sealed supertypes) +1 error + +$ javac broken/AnonymousSealed.java (21.0.10+7-Ubuntu-124.04) +broken/AnonymousSealed.java:5: error: anonymous classes must not extend sealed classes + return new Payment() {}; // an anonymous class is a subtype nobody listed + ^ +1 error + +$ javac broken/AnonymousSealed.java (25.0.4.1+1-LTS) +broken/AnonymousSealed.java:5: error: anonymous classes must not extend sealed classes + return new Payment() {}; // an anonymous class is a subtype nobody listed + ^ +1 error + +$ javac broken/AnonymousSealed.java (27+35) +broken/AnonymousSealed.java:5: error: anonymous classes must not extend sealed classes + return new Payment() {}; // an anonymous class is a subtype nobody listed + ^ +1 error + +$ javac broken/xpkg/a/Base.java broken/xpkg/b/Impl.java (21.0.10+7-Ubuntu-124.04) +broken/xpkg/a/Base.java:2: error: class Base in unnamed module cannot extend a sealed class in a different package +public sealed interface Base permits b.Impl {} + ^ +1 error + +$ javac broken/xpkg/a/Base.java broken/xpkg/b/Impl.java (25.0.4.1+1-LTS) +broken/xpkg/a/Base.java:2: error: class Base in unnamed module cannot extend a sealed class in a different package +public sealed interface Base permits b.Impl {} + ^ +1 error + +$ javac broken/xpkg/a/Base.java broken/xpkg/b/Impl.java (27+35) +broken/xpkg/a/Base.java:2: error: class Base in unnamed module cannot extend a sealed class in a different package +public sealed interface Base permits b.Impl {} + ^ +1 error diff --git a/sealed/output/04-default-trap.txt b/sealed/output/04-default-trap.txt new file mode 100644 index 0000000..78492e9 --- /dev/null +++ b/sealed/output/04-default-trap.txt @@ -0,0 +1,8 @@ +$ javac src/WithDefault.java && java WithDefault (21.0.10+7-Ubuntu-124.04) +fee for Wallet(1000) = 15 (the rule for Wallet is 1 percent, so 10; it compiled without any complaint) + +$ javac src/WithDefault.java && java WithDefault (25.0.4.1+1-LTS) +fee for Wallet(1000) = 15 (the rule for Wallet is 1 percent, so 10; it compiled without any complaint) + +$ javac src/WithDefault.java && java WithDefault (27+35) +fee for Wallet(1000) = 15 (the rule for Wallet is 1 percent, so 10; it compiled without any complaint) diff --git a/sealed/output/05-generic-and-recursive.txt b/sealed/output/05-generic-and-recursive.txt new file mode 100644 index 0000000..793b9ae --- /dev/null +++ b/sealed/output/05-generic-and-recursive.txt @@ -0,0 +1,26 @@ +$ javac src/ResultDemo.java && java ResultDemo (21.0.10+7-Ubuntu-124.04) +21 -> ok 42 +abc -> error: not a number: abc + +$ javac src/ResultDemo.java && java ResultDemo (25.0.4.1+1-LTS) +21 -> ok 42 +abc -> error: not a number: abc + +$ javac src/ResultDemo.java && java ResultDemo (27+35) +21 -> ok 42 +abc -> error: not a number: abc + +$ javac src/ExprDemo.java && java ExprDemo (21.0.10+7-Ubuntu-124.04) +Add[left=Mul[left=Num[value=1], right=Num[value=7]], right=Neg[inner=Neg[inner=Num[value=3]]]] +eval = 10 +simplify = Add[left=Num[value=7], right=Num[value=3]] (eval 10) + +$ javac src/ExprDemo.java && java ExprDemo (25.0.4.1+1-LTS) +Add[left=Mul[left=Num[value=1], right=Num[value=7]], right=Neg[inner=Neg[inner=Num[value=3]]]] +eval = 10 +simplify = Add[left=Num[value=7], right=Num[value=3]] (eval 10) + +$ javac src/ExprDemo.java && java ExprDemo (27+35) +Add[left=Mul[left=Num[value=1], right=Num[value=7]], right=Neg[inner=Neg[inner=Num[value=3]]]] +eval = 10 +simplify = Add[left=Num[value=7], right=Num[value=3]] (eval 10) diff --git a/sealed/output/06-visitor.txt b/sealed/output/06-visitor.txt new file mode 100644 index 0000000..6671788 --- /dev/null +++ b/sealed/output/06-visitor.txt @@ -0,0 +1,41 @@ +$ javac src/VisitorVersion.java && java VisitorVersion (21.0.10+7-Ubuntu-124.04) +Card[last4=4242, amount=500] -> fee 10 +Upi[vpa=ankur@bank, amount=500] -> fee 0 +NetBanking[bank=HDFC, amount=500] -> fee 15 + +$ javac src/VisitorVersion.java && java VisitorVersion (25.0.4.1+1-LTS) +Card[last4=4242, amount=500] -> fee 10 +Upi[vpa=ankur@bank, amount=500] -> fee 0 +NetBanking[bank=HDFC, amount=500] -> fee 15 + +$ javac src/VisitorVersion.java && java VisitorVersion (27+35) +Card[last4=4242, amount=500] -> fee 10 +Upi[vpa=ankur@bank, amount=500] -> fee 0 +NetBanking[bank=HDFC, amount=500] -> fee 15 + +$ javac broken/VisitorAdded.java (21.0.10+7-Ubuntu-124.04) +broken/VisitorAdded.java:9: error: Fee is not abstract and does not override abstract method visitWallet(Wallet) in Visitor + static class Fee implements Visitor { + ^ +broken/VisitorAdded.java:13: error: Label is not abstract and does not override abstract method visitWallet(Wallet) in Visitor + static class Label implements Visitor { + ^ +2 errors + +$ javac broken/VisitorAdded.java (25.0.4.1+1-LTS) +broken/VisitorAdded.java:9: error: Fee is not abstract and does not override abstract method visitWallet(Wallet) in Visitor + static class Fee implements Visitor { + ^ +broken/VisitorAdded.java:13: error: Label is not abstract and does not override abstract method visitWallet(Wallet) in Visitor + static class Label implements Visitor { + ^ +2 errors + +$ javac broken/VisitorAdded.java (27+35) +broken/VisitorAdded.java:9: error: Fee is not abstract and does not override abstract method visitWallet(Wallet) in Visitor + static class Fee implements Visitor { + ^ +broken/VisitorAdded.java:13: error: Label is not abstract and does not override abstract method visitWallet(Wallet) in Visitor + static class Label implements Visitor { + ^ +2 errors diff --git a/sealed/output/07-runtime-enforcement.txt b/sealed/output/07-runtime-enforcement.txt new file mode 100644 index 0000000..9296352 --- /dev/null +++ b/sealed/output/07-runtime-enforcement.txt @@ -0,0 +1,14 @@ +$ java -cp v1+Hack.class Main (Hack was compiled against a Payment that permitted it) (21.0.10+7-Ubuntu-124.04) +loading Card: Card +loading Hack... +Exception in thread "main" java.lang.IncompatibleClassChangeError: class Hack cannot implement sealed interface Payment + +$ java -cp v1+Hack.class Main (Hack was compiled against a Payment that permitted it) (25.0.4.1+1-LTS) +loading Card: Card +loading Hack... +Exception in thread "main" java.lang.IncompatibleClassChangeError: Failed listed permitted subclass check: class Hack is not a permitted subclass of Payment + +$ java -cp v1+Hack.class Main (Hack was compiled against a Payment that permitted it) (27+35) +loading Card: Card +loading Hack... +Exception in thread "main" java.lang.IncompatibleClassChangeError: Failed listed permitted subclass check: class Hack is not a permitted subclass of Payment diff --git a/sealed/output/08-modules.txt b/sealed/output/08-modules.txt new file mode 100644 index 0000000..9fc6f8b --- /dev/null +++ b/sealed/output/08-modules.txt @@ -0,0 +1,8 @@ +$ javac -d out $(find multi/demo.pay -name '*.java') && java -p out -m demo.pay/pay.app.Main (21.0.10+7-Ubuntu-124.04) +card in module demo.pay + +$ javac -d out $(find multi/demo.pay -name '*.java') && java -p out -m demo.pay/pay.app.Main (25.0.4.1+1-LTS) +card in module demo.pay + +$ javac -d out $(find multi/demo.pay -name '*.java') && java -p out -m demo.pay/pay.app.Main (27+35) +card in module demo.pay diff --git a/sealed/run.sh b/sealed/run.sh new file mode 100755 index 0000000..436c54f --- /dev/null +++ b/sealed/run.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Sealed classes and interfaces: permits rules, exhaustive switch without default, the traps around them, and what the JVM enforces at run time. +# Regenerates every file in output/. JDK25=/path JDK27=/path [JDK21=/path] ./run.sh +set -uo pipefail +unset JAVA_TOOL_OPTIONS +HERE="$(cd "$(dirname "$0")" && pwd)" +JDK25="${JDK25:-/opt/jdks/jdk-25.0.4.1+1}"; JDK27="${JDK27:-/opt/jdks/jdk27}"; JDK21="${JDK21:-}" +OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT +cd "$HERE" +t() { "$1/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'; } +mk() { mktemp -d -p "$B"; } +# compile + run +cr() { local jdk="$1" src="$2" cls="$3" o; o="$(mk)" + echo "\$ javac $src && java $cls ($(t "$jdk"))" + "$jdk/bin/javac" -d "$o" "$src" 2>&1 && "$jdk/bin/java" -cp "$o" "$cls" 2>&1; } +# compile only, show diagnostics +co() { local jdk="$1" src="$2" o; o="$(mk)" + echo "\$ javac $src ($(t "$jdk"))" + "$jdk/bin/javac" -d "$o" "$src" 2>&1 | grep -v '^Note:'; } +all3() { local fn="$1"; shift; [ -n "$JDK21" ] && { "$fn" "$JDK21" "$@"; echo; }; "$fn" "$JDK25" "$@"; echo; "$fn" "$JDK27" "$@"; } + +# 01 - the payment model +{ all3 cr src/PaymentDemo.java PaymentDemo; } > "$OUT/01-payment-demo.txt" + +# 02 - permits, the three modifiers, reflection and the class-file attribute +{ all3 cr src/Reflect.java Reflect; echo; all3 cr src/ImplicitPermits.java ImplicitPermits; echo + o="$(mk)"; "$JDK25/bin/javac" -d "$o" src/PaymentDemo.java + echo "\$ javap -v -cp . 'PaymentDemo\$Payment' | grep -A3 PermittedSubclasses ($(t "$JDK25"))" + "$JDK25/bin/javap" -v -cp "$o" 'PaymentDemo$Payment' | grep -A3 '^PermittedSubclasses'; } > "$OUT/02-permits-and-modifiers.txt" + +# 03 - the compile errors, one file each +{ for f in WalletNotHandled NotPermitted MissingModifier NoSubclass NonSealedWrongPlace AnonymousSealed; do all3 co broken/$f.java; echo; done + xp() { local jdk="$1" o; o="$(mk)"; echo "\$ javac broken/xpkg/a/Base.java broken/xpkg/b/Impl.java ($(t "$jdk"))" + "$jdk/bin/javac" -d "$o" broken/xpkg/a/Base.java broken/xpkg/b/Impl.java 2>&1; } + all3 xp; } > "$OUT/03-compile-errors.txt" + +# 04 - a default branch turns the check off +{ all3 cr src/WithDefault.java WithDefault; } > "$OUT/04-default-trap.txt" + +# 05 - generic and recursive hierarchies +{ all3 cr src/ResultDemo.java ResultDemo; echo; all3 cr src/ExprDemo.java ExprDemo; } > "$OUT/05-generic-and-recursive.txt" + +# 06 - the visitor, before sealed types +{ all3 cr src/VisitorVersion.java VisitorVersion; echo; all3 co broken/VisitorAdded.java; } > "$OUT/06-visitor.txt" + +# 07 - what the JVM enforces at run time (a class file whose permits clause was different when it was compiled) +rt() { local jdk="$1" o; o="$(mk)"; mkdir "$o/v1" "$o/evil" + "$jdk/bin/javac" -d "$o/v1" sep/v1/*.java sep/Main.java 2>&1; "$jdk/bin/javac" -d "$o/evil" sep/evil/*.java 2>&1 + cp "$o/evil/Hack.class" "$o/v1/" + echo "\$ java -cp v1+Hack.class Main (Hack was compiled against a Payment that permitted it) ($(t "$jdk"))" + "$jdk/bin/java" -cp "$o/v1" Main 2>&1 | head -3; } +{ all3 rt; } > "$OUT/07-runtime-enforcement.txt" + +# 08 - sealed across packages needs a named module +mm() { local jdk="$1" o; o="$(mk)" + echo "\$ javac -d out \$(find multi/demo.pay -name '*.java') && java -p out -m demo.pay/pay.app.Main ($(t "$jdk"))" + "$jdk/bin/javac" -d "$o" $(find multi/demo.pay -name '*.java') 2>&1 && "$jdk/bin/java" -p "$o" -m demo.pay/pay.app.Main 2>&1; } +{ all3 mm; } > "$OUT/08-modules.txt" +echo done diff --git a/sealed/sep/Main.java b/sealed/sep/Main.java new file mode 100644 index 0000000..0e5faaa --- /dev/null +++ b/sealed/sep/Main.java @@ -0,0 +1,7 @@ +public class Main { + public static void main(String[] args) throws Exception { + System.out.println("loading Card: " + Class.forName("Card").getSimpleName()); + System.out.println("loading Hack..."); + System.out.println(Class.forName("Hack").getSimpleName()); + } +} diff --git a/sealed/sep/evil/Card.java b/sealed/sep/evil/Card.java new file mode 100644 index 0000000..cdae0ee --- /dev/null +++ b/sealed/sep/evil/Card.java @@ -0,0 +1 @@ +public final class Card implements Payment {} diff --git a/sealed/sep/evil/Hack.java b/sealed/sep/evil/Hack.java new file mode 100644 index 0000000..b15afe5 --- /dev/null +++ b/sealed/sep/evil/Hack.java @@ -0,0 +1 @@ +public final class Hack implements Payment {} diff --git a/sealed/sep/evil/Payment.java b/sealed/sep/evil/Payment.java new file mode 100644 index 0000000..426a8a5 --- /dev/null +++ b/sealed/sep/evil/Payment.java @@ -0,0 +1 @@ +public sealed interface Payment permits Card, Hack {} diff --git a/sealed/sep/v1/Card.java b/sealed/sep/v1/Card.java new file mode 100644 index 0000000..cdae0ee --- /dev/null +++ b/sealed/sep/v1/Card.java @@ -0,0 +1 @@ +public final class Card implements Payment {} diff --git a/sealed/sep/v1/Payment.java b/sealed/sep/v1/Payment.java new file mode 100644 index 0000000..7ea724c --- /dev/null +++ b/sealed/sep/v1/Payment.java @@ -0,0 +1 @@ +public sealed interface Payment permits Card {} diff --git a/sealed/src/ExprDemo.java b/sealed/src/ExprDemo.java new file mode 100644 index 0000000..90b4ffe --- /dev/null +++ b/sealed/src/ExprDemo.java @@ -0,0 +1,37 @@ +public class ExprDemo { + sealed interface Expr permits Num, Add, Mul, Neg {} + record Num(int value) implements Expr {} + record Add(Expr left, Expr right) implements Expr {} + record Mul(Expr left, Expr right) implements Expr {} + record Neg(Expr inner) implements Expr {} + + static int eval(Expr e) { + return switch (e) { + case Num(int v) -> v; + case Add(Expr l, Expr r) -> eval(l) + eval(r); + case Mul(Expr l, Expr r) -> eval(l) * eval(r); + case Neg(Expr i) -> -eval(i); + }; + } + + // Nested record patterns look two levels deep in one case. + static Expr simplify(Expr e) { + return switch (e) { + case Add(Num(int a), Num(int b)) -> new Num(a + b); + case Mul(Num(int a), Expr r) when a == 1 -> simplify(r); + case Neg(Neg(Expr inner)) -> simplify(inner); + case Add(Expr l, Expr r) -> new Add(simplify(l), simplify(r)); + case Mul(Expr l, Expr r) -> new Mul(simplify(l), simplify(r)); + case Neg(Expr i) -> new Neg(simplify(i)); + case Num n -> n; + }; + } + + public static void main(String[] args) { + Expr e = new Add(new Mul(new Num(1), new Num(7)), new Neg(new Neg(new Num(3)))); + System.out.println(e); + System.out.println("eval = " + eval(e)); + Expr s = simplify(e); + System.out.println("simplify = " + s + " (eval " + eval(s) + ")"); + } +} diff --git a/sealed/src/ImplicitPermits.java b/sealed/src/ImplicitPermits.java new file mode 100644 index 0000000..975b88d --- /dev/null +++ b/sealed/src/ImplicitPermits.java @@ -0,0 +1,10 @@ +import java.util.Arrays; + +// If the subclasses live in the same file, the permits clause can be left out: javac infers it. +public sealed class ImplicitPermits { + static final class A extends ImplicitPermits {} + static final class B extends ImplicitPermits {} + public static void main(String[] args) { + System.out.println(Arrays.stream(ImplicitPermits.class.getPermittedSubclasses()).map(Class::getName).toList()); + } +} diff --git a/sealed/src/PaymentDemo.java b/sealed/src/PaymentDemo.java new file mode 100644 index 0000000..c25f395 --- /dev/null +++ b/sealed/src/PaymentDemo.java @@ -0,0 +1,35 @@ +public class PaymentDemo { + // A Payment is exactly one of these three things, and nothing else. + sealed interface Payment permits Card, Upi, NetBanking {} + record Card(String last4, long amount) implements Payment {} + record Upi(String vpa, long amount) implements Payment {} + record NetBanking(String bank, long amount) implements Payment {} + + // No default branch: the compiler proves every permitted type is handled. + static long fee(Payment p) { + return switch (p) { + case Card(String last4, long amount) -> amount * 2 / 100; // 2 percent + case Upi(String vpa, long amount) -> 0; // free + case NetBanking(String bank, long amount) -> 15; // flat fee + }; + } + + static String describe(Payment p) { + return switch (p) { + case Card(String last4, long amount) when amount >= 10_000 -> "large card payment ending " + last4; + case Card(String last4, long amount) -> "card payment ending " + last4; + case Upi(String vpa, long amount) -> "UPI payment from " + vpa; + case NetBanking(String bank, long amount) -> "net banking via " + bank; + }; + } + + public static void main(String[] args) { + Payment[] all = { + new Card("4242", 500), new Card("1111", 25_000), + new Upi("ankur@bank", 500), new NetBanking("HDFC", 500) + }; + for (Payment p : all) { + System.out.println(p + " -> fee " + fee(p) + " | " + describe(p)); + } + } +} diff --git a/sealed/src/Reflect.java b/sealed/src/Reflect.java new file mode 100644 index 0000000..6ece5e6 --- /dev/null +++ b/sealed/src/Reflect.java @@ -0,0 +1,33 @@ +import java.lang.reflect.Modifier; +import java.util.Arrays; + +public class Reflect { + sealed interface Shape permits Circle, Poly, Custom {} + record Circle(double r) implements Shape {} // records are implicitly final + sealed interface Poly extends Shape permits Tri, Quad {} // sealed again: the hierarchy can be deeper + record Tri(double b, double h) implements Poly {} + static final class Quad implements Poly {} // an explicit final class + non-sealed interface Custom extends Shape {} // re-opens the hierarchy below this point + static class Hexagon implements Custom {} // legal, and not in any permits clause + + static void show(Class c) { + String kind = c.isSealed() ? "sealed" : Modifier.isFinal(c.getModifiers()) ? "final" : "open (non-sealed or plain)"; + System.out.println(c.getSimpleName() + ": " + kind + + (c.isSealed() ? ", permits " + Arrays.stream(c.getPermittedSubclasses()).map(Class::getSimpleName).toList() : "")); + } + + // Custom is covered by one case; Hexagon and any future subtype of Custom are covered by it too. + static String name(Shape s) { + return switch (s) { + case Circle c -> "circle"; + case Tri t -> "triangle"; + case Quad q -> "quad"; + case Custom c -> "custom:" + c.getClass().getSimpleName(); + }; + } + + public static void main(String[] args) { + for (Class c : new Class[]{Shape.class, Circle.class, Poly.class, Tri.class, Quad.class, Custom.class, Hexagon.class}) show(c); + System.out.println(name(new Circle(1)) + " " + name(new Tri(1, 2)) + " " + name(new Quad()) + " " + name(new Hexagon())); + } +} diff --git a/sealed/src/ResultDemo.java b/sealed/src/ResultDemo.java new file mode 100644 index 0000000..1b82167 --- /dev/null +++ b/sealed/src/ResultDemo.java @@ -0,0 +1,31 @@ +import java.util.function.Function; + +public class ResultDemo { + // A generic sealed type: a Result is either an Ok holding a T or an Err holding a message. + sealed interface Result permits Ok, Err {} + record Ok(T value) implements Result {} + record Err(String message) implements Result {} + + static Result map(Result r, Function f) { + return switch (r) { + case Ok(T value) -> new Ok<>(f.apply(value)); + case Err(String message) -> new Err<>(message); + }; + } + + static Result parse(String s) { + try { return new Ok<>(Integer.parseInt(s)); } + catch (NumberFormatException e) { return new Err<>("not a number: " + s); } + } + + public static void main(String[] args) { + for (String s : new String[]{"21", "abc"}) { + Result doubled = map(parse(s), n -> n * 2); + String text = switch (doubled) { + case Ok(Integer v) -> "ok " + v; + case Err(String m) -> "error: " + m; + }; + System.out.println(s + " -> " + text); + } + } +} diff --git a/sealed/src/VisitorVersion.java b/sealed/src/VisitorVersion.java new file mode 100644 index 0000000..4e1d7f4 --- /dev/null +++ b/sealed/src/VisitorVersion.java @@ -0,0 +1,19 @@ +public class VisitorVersion { + // The pre-sealed way: a visitor interface, an accept method on every type, and one implementation per operation. + interface Payment { R accept(Visitor v); } + interface Visitor { R visitCard(Card c); R visitUpi(Upi u); R visitNetBanking(NetBanking n); } + record Card(String last4, long amount) implements Payment { public R accept(Visitor v) { return v.visitCard(this); } } + record Upi(String vpa, long amount) implements Payment { public R accept(Visitor v) { return v.visitUpi(this); } } + record NetBanking(String bank, long amount) implements Payment { public R accept(Visitor v) { return v.visitNetBanking(this); } } + + static class Fee implements Visitor { + public Long visitCard(Card c) { return c.amount() * 2 / 100; } + public Long visitUpi(Upi u) { return 0L; } + public Long visitNetBanking(NetBanking n) { return 15L; } + } + + public static void main(String[] args) { + Payment[] all = { new Card("4242", 500), new Upi("ankur@bank", 500), new NetBanking("HDFC", 500) }; + for (Payment p : all) System.out.println(p + " -> fee " + p.accept(new Fee())); + } +} diff --git a/sealed/src/WithDefault.java b/sealed/src/WithDefault.java new file mode 100644 index 0000000..ed076d0 --- /dev/null +++ b/sealed/src/WithDefault.java @@ -0,0 +1,20 @@ +public class WithDefault { + sealed interface Payment permits Card, Upi, NetBanking, Wallet {} // Wallet was added later + record Card(long amount) implements Payment {} + record Upi(long amount) implements Payment {} + record NetBanking(long amount) implements Payment {} + record Wallet(long amount) implements Payment {} + + // Written back when there were only three kinds. The default keeps it compiling, and keeps it wrong. + static long feeWithDefault(Payment p) { + return switch (p) { + case Card c -> c.amount() * 2 / 100; + case Upi u -> 0; + default -> 15; + }; + } + + public static void main(String[] args) { + System.out.println("fee for Wallet(1000) = " + feeWithDefault(new Wallet(1000)) + " (the rule for Wallet is 1 percent, so 10; it compiled without any complaint)"); + } +}