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
+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)");
}
}