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:
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
public class MissingModifier {
|
||||
sealed interface Payment permits Card {}
|
||||
class Card implements Payment {} // must say final, sealed or non-sealed
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
public class NoSubclass {
|
||||
sealed interface Payment {} // no permits clause and no subtype in this 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package a;
|
||||
public sealed interface Base permits b.Impl {}
|
||||
@@ -0,0 +1,2 @@
|
||||
package b;
|
||||
public final class Impl implements a.Base {}
|
||||
@@ -0,0 +1 @@
|
||||
module demo.pay { }
|
||||
@@ -0,0 +1,2 @@
|
||||
package pay.api;
|
||||
public sealed interface Payment permits pay.impl.Card, pay.impl.Upi {}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package pay.impl;
|
||||
public record Card(long amount) implements pay.api.Payment {}
|
||||
@@ -0,0 +1,2 @@
|
||||
package pay.impl;
|
||||
public record Upi(long amount) implements pay.api.Payment {}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
public final class Card implements Payment {}
|
||||
@@ -0,0 +1 @@
|
||||
public final class Hack implements Payment {}
|
||||
@@ -0,0 +1 @@
|
||||
public sealed interface Payment permits Card, Hack {}
|
||||
@@ -0,0 +1 @@
|
||||
public final class Card implements Payment {}
|
||||
@@ -0,0 +1 @@
|
||||
public sealed interface Payment permits Card {}
|
||||
@@ -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) + ")");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user