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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
Claude
2026-09-24 12:03:59 +00:00
parent 65a4bbbbe3
commit ff6130bede
38 changed files with 638 additions and 0 deletions
+15
View File
@@ -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.
+7
View File
@@ -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
}
}
+4
View File
@@ -0,0 +1,4 @@
public class MissingModifier {
sealed interface Payment permits Card {}
class Card implements Payment {} // must say final, sealed or non-sealed
}
+3
View File
@@ -0,0 +1,3 @@
public class NoSubclass {
sealed interface Payment {} // no permits clause and no subtype in this file
}
+4
View File
@@ -0,0 +1,4 @@
public class NonSealedWrongPlace {
interface Plain {}
non-sealed class Oops implements Plain {} // non-sealed only makes sense under a sealed supertype
}
+6
View File
@@ -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
}
+17
View File
@@ -0,0 +1,17 @@
public class VisitorAdded {
interface Payment { <R> R accept(Visitor<R> v); }
interface Visitor<R> { R visitCard(Card c); R visitUpi(Upi u); R visitWallet(Wallet w); } // Wallet added here
record Card() implements Payment { public <R> R accept(Visitor<R> v) { return v.visitCard(this); } }
record Upi() implements Payment { public <R> R accept(Visitor<R> v) { return v.visitUpi(this); } }
record Wallet() implements Payment { public <R> R accept(Visitor<R> v) { return v.visitWallet(this); } }
// Two existing operations, neither knows about Wallet: each one now fails to compile.
static class Fee implements Visitor<Long> {
public Long visitCard(Card c) { return 2L; }
public Long visitUpi(Upi u) { return 0L; }
}
static class Label implements Visitor<String> {
public String visitCard(Card c) { return "card"; }
public String visitUpi(Upi u) { return "upi"; }
}
}
+16
View File
@@ -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;
};
}
}
+2
View File
@@ -0,0 +1,2 @@
package a;
public sealed interface Base permits b.Impl {}
+2
View File
@@ -0,0 +1,2 @@
package b;
public final class Impl implements a.Base {}
+1
View File
@@ -0,0 +1 @@
module demo.pay { }
@@ -0,0 +1,2 @@
package pay.api;
public sealed interface Payment permits pay.impl.Card, pay.impl.Upi {}
+10
View File
@@ -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());
}
}
+2
View File
@@ -0,0 +1,2 @@
package pay.impl;
public record Card(long amount) implements pay.api.Payment {}
+2
View File
@@ -0,0 +1,2 @@
package pay.impl;
public record Upi(long amount) implements pay.api.Payment {}
+17
View File
@@ -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
@@ -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
+130
View File
@@ -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
+8
View File
@@ -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)
@@ -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)
+41
View File
@@ -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<Long> {
^
broken/VisitorAdded.java:13: error: Label is not abstract and does not override abstract method visitWallet(Wallet) in Visitor
static class Label implements Visitor<String> {
^
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<Long> {
^
broken/VisitorAdded.java:13: error: Label is not abstract and does not override abstract method visitWallet(Wallet) in Visitor
static class Label implements Visitor<String> {
^
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<Long> {
^
broken/VisitorAdded.java:13: error: Label is not abstract and does not override abstract method visitWallet(Wallet) in Visitor
static class Label implements Visitor<String> {
^
2 errors
+14
View File
@@ -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
+8
View File
@@ -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
Executable
+59
View File
@@ -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
+7
View File
@@ -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());
}
}
+1
View File
@@ -0,0 +1 @@
public final class Card implements Payment {}
+1
View File
@@ -0,0 +1 @@
public final class Hack implements Payment {}
+1
View File
@@ -0,0 +1 @@
public sealed interface Payment permits Card, Hack {}
+1
View File
@@ -0,0 +1 @@
public final class Card implements Payment {}
+1
View File
@@ -0,0 +1 @@
public sealed interface Payment permits Card {}
+37
View File
@@ -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) + ")");
}
}
+10
View File
@@ -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());
}
}
+35
View File
@@ -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));
}
}
}
+33
View File
@@ -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()));
}
}
+31
View File
@@ -0,0 +1,31 @@
import java.util.function.Function;
public class ResultDemo {
// A generic sealed type: a Result<T> is either an Ok holding a T or an Err holding a message.
sealed interface Result<T> permits Ok, Err {}
record Ok<T>(T value) implements Result<T> {}
record Err<T>(String message) implements Result<T> {}
static <T, R> Result<R> map(Result<T> r, Function<T, R> f) {
return switch (r) {
case Ok<T>(T value) -> new Ok<>(f.apply(value));
case Err<T>(String message) -> new Err<>(message);
};
}
static Result<Integer> 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<Integer> doubled = map(parse(s), n -> n * 2);
String text = switch (doubled) {
case Ok<Integer>(Integer v) -> "ok " + v;
case Err<Integer>(String m) -> "error: " + m;
};
System.out.println(s + " -> " + text);
}
}
}
+19
View File
@@ -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> R accept(Visitor<R> v); }
interface Visitor<R> { R visitCard(Card c); R visitUpi(Upi u); R visitNetBanking(NetBanking n); }
record Card(String last4, long amount) implements Payment { public <R> R accept(Visitor<R> v) { return v.visitCard(this); } }
record Upi(String vpa, long amount) implements Payment { public <R> R accept(Visitor<R> v) { return v.visitUpi(this); } }
record NetBanking(String bank, long amount) implements Payment { public <R> R accept(Visitor<R> v) { return v.visitNetBanking(this); } }
static class Fee implements Visitor<Long> {
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()));
}
}
+20
View File
@@ -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)");
}
}