record 一行定义不可变数据, sealed 封闭继承域, switch 模式匹配带穷尽检查 — Java 8 之后的三板斧, 传统样板代码的葬礼
record 解决的是"数据载体的样板": 一个 DTO 真正要写的只有字段列表, 其余 40 行都是仪式 — record 把仪式交给编译器, 你写一行, 它生成全套且不会漏 equals。sealed 解决的是"继承域的失控": 谁能实现我的接口由我圈死, 于是编译器第一次"知道全部可能" — 这正是穷尽 switch 的地基: 漏一个分支编译不过, 新增子类全项目报错提醒, 相当于用编译器做需求评审。
三板斧合体的杀伤力: DTO 用 record, 业务域用 sealed interface 圈住, 分发逻辑用 switch 表达式 — if-else 长链、instanceof 强转链、忘改分支的事故, 一起进坟场。配套的 var/文本块/instanceof 模式匹配是零碎的减负件: var 只在局部且右侧类型明显时用, 文本块的缩进由结尾定界符说了算。
id() (不是 getId)、按组件值的 equals/hashCode/toString、final 类 + final 字段。它是"不可变数据的载体", 不是"少写 getter 的技巧"。
record Point(int x, int y) {} // JDK16+ 一行 = 全套样板 new Point(1, 2).x(); // → 1 访问器是 x() 不是 getX new Point(1, 2).equals(new Point(1, 2)); // → true 按组件值比较
public Money { 校验/规范化 } 无参数列表形态: 先改参数, 编译器再隐式赋给字段 — 把"非法状态不可表示"的防线挪到对象诞生前。
public record Money(BigDecimal amount) { public Money { // 关键: 无参数列表 = 紧凑形态 if (amount.signum() < 0) throw new IllegalArgumentException("<0"); } } new Money(new BigDecimal("-1")); // → IllegalArgumentException
// Jackson 2.12+: record 原生反序列化 OrderDTO dto = mapper.readValue(json, OrderDTO.class); // JPA Entity ✗ 要无参构造+可变; 投影/DTO ✓ public record OrderView(String id) implements OrderProjection {}
sealed interface Shape permits Circle, Rect, Square: 继承域封闭, 子类必须 final/sealed/non-sealed 三选一; 子类需同模块 (无模块则同包)。价值: 编译器知道"全部可能"。
public sealed interface Shape permits Circle, Rect {} // JDK17+ public record Circle(double r) implements Shape {} // record 隐式 final // class Triangle implements Shape {} → 编译错: 不在 permits 域内
case Circle c: 判断类型 + 绑定变量一步完成, 替代 instanceof + 强转样板; c 的作用域仅限该分支。
if (shape instanceof Circle c && c.r() > 1) { // JDK16+ 判型+绑定 area = c.r() * c.r() * Math.PI; // c 已是 Circle, 无强转 } // 关键: c 仅此分支内可见
case Circle(Point p, double r): 按组件拆开嵌套取值 (Java 21 正式), 只有 record 能被解构 — 普通类没有"组件"概念。
double d = switch (shape) { // JDK21+ 解构模式 case Circle(double r) -> Math.PI * r * r; case Rect(double w, double h) -> w * h; }; // 关键: 只有 record 有"组件"可解构, 普通类不行
double a = switch (shape) { // sealed 域, 不写 default case Circle c -> c.r() * c.r() * Math.PI; case Rect r -> r.w() * r.h(); }; // 删任一分支 → 编译错 does not cover all possible values
int n = switch (x) { case A -> 1; default -> { yield calc(); } }; 箭头 case 无 fall-through, 块内用 yield 产值, 冒号 case 也能配 yield。
int n = switch (level) { // JDK14+ 有值可接 case 1, 2 -> 10; // 箭头 case 无 fall-through default -> { int v = bonus(); yield v; } // 块内 yield 产值 };
case WechatChannel w when w.isVip() -> ...: 同类型下再筛条件; 注意守卫分支不参与穷尽检查, 其后要有兜底分支。
BigDecimal fee = switch (channel) { // JDK21+ 守卫 when case Wechat w when w.isVip() -> BigDecimal.ZERO; // 同型再筛 case Wechat w -> w.rate(); case Alipay a -> a.rate(); // 守卫不参与穷尽, 留兜底 };
var orders = new ArrayList<Order>(); // JDK10+ ✓ 右侧类型可见 // var x = service.process(ctx); ✗ 类型被抹, review 猜谜 // 字段/参数/返回值用 var → 编译错; 它是编译期糖, 非动态类型
""" 三引号多行字符串: 收尾定界符所在列参与决定最小公共缩进剥离; 行尾 \ 续行不换行、\s 保留尾随空格 — JSON/SQL/日志模板专用。
String json = """ { "city": "杭州" } """; // JDK15+ → { "city": "杭州" } 无多余缩进
if (o instanceof String s) 判型即绑定, 流敏感作用域: 只在"确定匹配"的分支可见, 复合条件里编译器替你把关。
if (o instanceof String s && !s.isBlank()) { // JDK16+ log(s.trim()); // s 只在"确定匹配"分支可见 } // 写进 else 分支 → 编译器直接拦
String city = userRepo.findById(uid) // 仓储返回 Optional .map(User::city) .orElse("未知"); // → 有值取值, 无值兜底 // 关键: 只做返回值; 字段/参数/裸 get() 都是反模式
Lombok @Value 靠注解处理器魔改字节码, 升 JDK 常踩坑; record 是语言原生, 依赖链先升级再迁移:
// 迁移前置: jackson 升 2.12+; 编译参数加 -parameters (保住组件参数名) public record OrderDTO(String id, BigDecimal amount, Status status) {} // 注解直接贴组件上, 反序列化字段映射照常工作 public record OrderDTO( @JsonProperty("order_id") String id, @JsonProperty("pay_amount") BigDecimal amount, Status status) {} // record 是语言特性: 调试器/反射/IDE 全原生支持, 不依赖 lombok 插件 // 判据: 出入参 DTO/内部值对象 → record; 有行为有状态的领域对象 → class
脏数据从网关进来, 与其在每个使用点判空判负, 不如在对象诞生时收口:
// 紧凑构造器: 校验/规范化写这里, 不必重写整套装配件 public record Money(BigDecimal amount, Currency currency) { public Money { // 无参数列表 = 紧凑形态 amount = amount.setScale(2, RoundingMode.HALF_UP); // 统一 scale if (amount.signum() < 0) throw new IllegalArgumentException("amount < 0: " + amount); } // 非法值挡在对象诞生前 } // 反模式: 在这里写 this.amount = ... — 编译错, 字段赋值由编译器收尾
支付渠道是典型封闭集合 — 漏改一个渠道的费率/对账逻辑就是资损事故, 用 sealed 把风险变成编译错:
// permits 圈死实现集 — 加渠道漏改分支? 编译器追着你改 public sealed interface PaymentChannel permits AlipayChannel, WechatChannel, CardChannel {} public record AlipayChannel(String appId) implements PaymentChannel {} public record WechatChannel(String mchId) implements PaymentChannel {} public record CardChannel(String bin) implements PaymentChannel {} // 新接 UnionPay 忘了改费率 switch → 缺分支, mvn compile 直接红, 上不了线
渠道费率原来是一串 if-else, 新增渠道忘改就静默走 else 兜底 — 换成穷尽 switch 后这成了不可能事件:
// if-else 长链 → switch 表达式: 有返回值 + 穷尽 + 无 fall-through BigDecimal fee = switch (channel) { case AlipayChannel a -> a.amount().multiply(new BigDecimal("0.006")); case WechatChannel w when w.isVip() -> BigDecimal.ZERO; // 守卫子条件 case WechatChannel w -> w.amount().multiply(new BigDecimal("0.005")); case CardChannel c -> c.amount().multiply(new BigDecimal("0.012")); }; // sealed 穷尽: 运行时走不到"没想过"的路径
拼接 JSON 的引号转义地狱 + SQL 的 + 换行, 文本块一次解决, 缩进行为由收尾定界符说了算:
// 内嵌 JSON 模板: 告别引号转义地狱; 缩进由结尾 """ 所在列决定 String body = """ { "city": "杭州", "tags": ["new", "vip"] } """; // 此列左侧的公共缩进整体剥掉 // SQL: 行尾 \ 续行不换行; \s 显式保留尾随空格 (默认会被剥) String sql = """ select id, amount \ from t_order where city = '杭州'\ """;
遗留代码里三层嵌套的 instanceof + (String) 强转, 模式匹配一步到位且作用域严格:
// 消强转: 类型判断 + 变量绑定一步完成, 变量作用域只在分支内 if (obj instanceof String s && !s.isBlank()) { log.info("raw string: {}", s.trim()); // s 已是 String, 无强转 } else if (obj instanceof List<?> list && list.size() > 100) { log.warn("oversized list: {}", list.size()); } // 流敏感: 条件成立 s 才可用 — 写到 else 里去编译器直接拦下
record 虽不能继承类, 但能实现接口 — 数据携带工厂与解析逻辑, 构造语义显式化:
// record 实现接口 + 静态工厂: 默认值收口, 非法值早爆 public record PageQuery(int page, int size) implements Query { public static PageQuery of(int page, int size) { if (size > 200) throw new IllegalArgumentException("size > 200"); return new PageQuery(Math.max(page, 1), size); // 默认值收口在工厂 } public static PageQuery parse(Map<String, String> q) { return of(Integer.parseInt(q.getOrDefault("page", "1")), Integer.parseInt(q.getOrDefault("size", "20"))); } }
事件路由表原来是 20 行 if-else 且新增事件要人肉记得改 — 封闭域上的穷尽 switch 让编译器当检查员:
// 分发逻辑从 if-else 长链变成封闭域上的穷尽 switch public sealed interface Event permits OrderPaid, OrderRefunded, CartUpdated {} public record Route(String table, String topic) {} public Route route(Event e) { return switch (e) { // 新事件上线 → 这里编译报错 case OrderPaid p -> new Route("t_paid", "paid-topic"); case OrderRefunded r -> new Route("t_refund", "refund-topic"); case CartUpdated c -> new Route("t_cart", "cart-topic"); }; }
var 用好了减噪, 用烂了 review 变猜谜 — 四条规约写进团队手册, 满足才许用:
// var 团队规约: 只用局部变量, 且右侧类型"一眼可见" var orders = new ArrayList<OrderDTO>(); // ✓ 右侧类型明确 var map = groupByCity(orders); // ✗ 要翻方法签名才知道类型 try (var in = Files.newInputStream(path)) { // ✓ 资源具体类型无关紧要 in.readAllBytes(); } for (var e : orders.entrySet()) { log.debug("{}", e.getKey()); } // ✓ 减噪 // var 不能上字段/参数/返回值 — 它只是局部的类型推断, 不是动态类型
仓储层返回 null 逼着调用方层层设防 — Optional 把"可能没有"变成显式契约, 兜底策略就地表达:
// Optional 只做返回值: 链式取值 + 显式兜底, 拒绝裸 get() public City resolveCity(long uid) { return Optional.ofNullable(userRepo.find(uid)) // 仓储本就该返回 Optional .map(User::profile) .map(Profile::address) .map(Address::city) .orElseThrow(() -> new NotFound("user " + uid)); // 缺失走异常 } // 禁: Optional 做字段/参数/get() — 序列化炸, 签名噪音, NoSuchElementException
// 错: class VipOrder extends OrderDTO {} // → cannot inherit from final // 对: record VipOrder(String id) implements Discount {} // 多态走接口 // record 隐式 extends java.lang.Record, 也不能再 extends 别的类
// 错: @Entity public record Order(Long id) {} // 启动报错/懒加载失效 // 对: @Entity public class Order { ... } // 实体保留 class // record OrderView(String id) {} ← 投影/DTO 用 record 没问题
-parameters, 必要时组件逐个标注。
// 错: jackson 2.11 或没编 -parameters → InvalidDefinitionException // 对: 升 jackson 2.12+; 编译带 -parameters; 必要时逐组件标注: public record OrderDTO(@JsonProperty("order_id") String id) {}
items = List.copyOf(items) 防御拷贝。
// 错: record Cart(List<String> items) {} // 引用不变 ≠ 内容不变 List<String> m = new ArrayList<>(List.of("a")); Cart c = new Cart(m); m.add("b"); // → c.items() 变 [a, b] // 对: public Cart { items = List.copyOf(items); } 防御拷贝
// 错: public Money { this.amount = amount; } → cannot assign final // 对: public Money { amount = amount.setScale(2, HALF_UP); } // 字段赋值是编译器的收尾动作, 抢了就编译错, 只改参数即可
// 错: Shape 在 com.app.core, Circle 在 com.app.web // → subclass of sealed class must be in same package/module // 对: 子类与接口同包声明, 或同一个 .java 文件里写一起
maven.compiler.release 锁死版本, 升级说明里记录语法边界。
// 错: JDK17 编译 case Point(int x, int y) → 编译不过 (解构 21 才正式) // 对: pom 锁死版本, 升级说明记录语法边界: // <maven.compiler.release>21</maven.compiler.release>
yield value;; 真要提前退出方法就别用 switch 表达式。
int n = switch (x) { case A -> { return 1; } // 错: return from within switch expression default -> { int v = calc(); yield v; } // 对: yield 产值 };
var x = service.process(...) review 时全靠猜, 泛型嵌套更灾难 (是 List 还是 Optional<List>?). 原因: 推断把类型信息从调用点抹掉. 正解: 团队规约"右侧不含类型名/构造器就不用 var"。
var r = service.process(ctx); // 错: List? Optional? review 猜谜 List<Order> r2 = service.process(ctx); // 对: 类型回到调用点 var orders = new ArrayList<Order>(); // 对: 右侧含构造器, 一眼可见
// 错: class A { var name; } / void f(var p) {} → var is not allowed here // 对: class A { String name; } void f(String p) {} 签名类型写全 var name = "ok"; // var 只属于方法内局部变量
""" 单独一行并顶到目标列, 疑难杂症用 .stripIndent() 打印验证。
String a = """ abc """; // 对: 收尾有缩进 → "abc" (公共缩进 4 被剥) String b = """ abc """; // 错: 收尾顶格公共缩进=0 → " abc" (4 空格保留)
\s 显式保留一个空格; 对齐敏感场景逐行检查。
// 错: 行尾空格默认剥除 — 拼签名/SQL 结果悄悄变了 String s = """ sign: alice\s """; // 对: \s 保留尾随空格 → "sign: alice "
if (!(o instanceof String s)) return; 后面用 s 报 "cannot find symbol". 原因: 流敏感作用域, 取反后"匹配成立"不被保证. 正解: 正向 if 包住使用处, 或改 Optional/map 链。
if (!(o instanceof String s)) return; log(s); // 错: cannot find symbol s if (o instanceof String s) log(s); // 对: 正向 if 包住使用处
record Tags(String[] arr) {} new Tags(new String[]{"a"}).equals(new Tags(new String[]{"a"})); // 错: → false record Tags2(List<String> arr) {} // 对: 同内容比较 → true
isSealed() 再取; 注意返回的是 Class 描述, 用前做可达性处理。
// 错: cls 非 sealed 时 getPermittedSubclasses() 返回 null → NPE if (cls.isSealed()) // 对: 先判再取 for (Class<?> c : cls.getPermittedSubclasses()) handle(c);
amountWithTax(); 真有可变状态就不是 record 的场景。
// 错: record Order(...) { private double cache; } → 实例字段必须静态 // 对: 派生值写成方法 double amountWithTax() { return amount().multiply(TAX); }
case Point(int x, int y) 对 class Point 编译错. 原因: 解构模式只支持 record 的组件访问器, 普通类没有"组件"概念. 正解: 建模改 record, 或退回类型模式 + getter。
class Point { int x, y; } // 错: case Point(int x, int y) → 普通类无组件, 编译错 record Point2(int x, int y) {} // 对: 改 record 才可解构 (JDK21+)
switch(shape) 传 null 直接 NPE; 模式匹配 switch 同样 NPE, 但 Java 21 起可 case null -> 显式接住. 正解: 入口判空或 case null 兜底, 别赌"不会是 null"。
double a = switch (shape) { case null -> 0; // 对: JDK21+ 显式接住 null case Circle c -> c.r() * c.r() * Math.PI; }; // 错: 不写 case null, 传 null → NullPointerException
orElseThrow(() -> new NotFound(...)) 带语义, 或 orElse 给默认。
opt.get(); // 错: → NoSuchElementException, 无业务语义 opt.orElseThrow(() -> new NotFound("user " + uid)); // 对 String v = opt.orElse("default"); // 对: 能兜底就兜底
\ 专用于续行. 正解: 正则照旧双写 \\d; 行尾真反斜杠写成 \\ 或前面加 \s 隔断。
// 错: 文本块里写 \d 当正则 → illegal escape; 行尾单 \ 被当续行拼掉 Pattern p = Pattern.compile(""" \\d{3}-\\d{4}"""); // 对: 照旧双写 → 正则 \d{3}-\d{4}