写了 20 年 Java,我发现 90% 的 if-null 和 try-catch 其实是因为缺了一条原则

你写过一百遍 if (str != null && !str.isEmpty()) { try { ... } catch (...) { ... } },却从没想过:"宽容"到底应该宽容到哪一步? Postel 定律统治了软件工程 45 年,没回答这个问题。这篇文章给出我的答案,并把它命名为 有界宽容原则(Bounded Leniency Principle)


一、先看一段你写过一百遍的代码

java 复制代码
String ageStr = (String) map.get("age");
int age;
if (ageStr == null || ageStr.isEmpty()) {
    age = 0;
} else {
    try {
        age = Integer.parseInt(ageStr.trim());
    } catch (NumberFormatException e) {
        age = 0;  // 吞掉?记日志?抛出去?每个人写的都不一样
    }
}

一个"取个年龄"的需求,写出 10 行模板代码。问题不在你菜,而在于------

Java 生态里,没人告诉你"宽容"应该宽容到哪一步。

  • map.get 不存在 → 返回 null(宽容)
  • Integer.parseInt(null) → 抛 NPE(不宽容)
  • Integer.parseInt("") → 抛异常(不宽容)
  • Integer.parseInt("abc") → 抛异常(应该不宽容)

四种情况,四种态度,毫无章法。每一个 Java 开发者都在用 try/catch 和 if-null 给这套混乱打补丁。

二、行业的两个极端,都不对

极端一:Postel 定律的"无限宽容"

"Be conservative in what you send, be liberal in what you accept." ------ RFC 793, 1981

听起来很有道理。但 45 年后的今天,IETF 自己发了 RFC 9413 反思 Postel 定律:HTML 解析器互相宽容兼容了二十年,结果是没人敢写一个新解析器------因为"宽容"已经变成了一坨没人能定义的玄学。

无限宽容 = 没有契约。

极端二:Fail-Fast 的"无限严格"

Jackson 默认 FAIL_ON_UNKNOWN_PROPERTIES = true,前端多传一个字段就 500。 JDK Integer.parseInt(null) 直接 NPE。

无限严格 = 把"用户没填"和"用户填错了"当成同一种错。

但这两件事根本不是一回事

  • 用户没填年龄 → 业务上很常见,不是错误
  • 用户填了 "abc" → 这是真错,必须立刻暴露

把它们混为一谈,要么逼业务代码写满 try/catch,要么让脏数据偷偷流进数据库。

三、有界宽容原则:在两个极端之间画一条线

我把数据访问 API 面对的输入分成三个正交维度

维度 含义 例子
存在性 (Missing) 值在不在 路径不存在、null""
形式 (Type) 类型对不对 "123"int1boolean
内容 (Content) 内容能不能解释 "abc"int

然后给出三条规则:

🟢 Rule 1:Missing-Lenient(缺失宽容)

值不存在时,返回 null 或默认值,绝不抛异常。 理由:缺失是业务常态,不是错误。

🟢 Rule 2:Type-Lenient(形式宽容)

类型不同但内容可解释时,自动转换。 理由:前端传 "123" 还是 123 不该由后端代码关心。

🔴 Rule 3:Content-Strict(内容严格)

内容根本无法解释时,立刻抛异常,绝不静默兜底。 理由:脏数据必须在入口暴露,而不是流到数据库才被发现。

一句话:对"没有"和"形不同"宽容,对"内容是垃圾"严格。

四、判定表:把原则变成机械可判定的契约

任何一个数据访问函数 f(input) → output,对照这张表就知道它合不合规:

输入情况 应有行为 违反者示例
路径不存在 返回 null / 默认值 Map.get ✅ / Integer.parseInt(null)
值为 null 返回 null / 默认值 多数库 ❌
值为 "" 返回 null / 默认值 Integer.parseInt("")
"123" → int 自动转换 JDK 原生 Map ❌
1 → boolean 自动转换 JDK ❌
"abc" → int 抛异常 Hutool Convert.toInt("abc", 0) 静默返回 0 ❌
"2024-13-45" → date 抛异常 LocalDate.parse

这张表是这个原则最有价值的产物:你不用再争论"这个 API 设计得对不对",对着表打勾就行。

五、用这个原则审判主流库

Missing Type Content 评价
JDK 原生 ❌ NPE ❌ ClassCastException ✅ 抛异常 三层全错前两层
Jackson 默认 ❌ 抛异常 部分 ✅ 缺失层过严
Gson ✅ null 部分 ✅ 部分静默 内容层有时太宽
Hutool Convert ❌ 静默兜底 内容层放水,脏数据天堂
Apache MapUtils 部分 ✅ ❌ 静默返回默认值 同上
有界宽容 ✅ 严格 三层都对

看出来了吗?主流库里居然没有一个三层都对的------这就是这个原则存在的意义。

六、违反它会怎样:一个真实事故

某次促销活动,前端 bug 让 discount 字段偶发传成 "undefined" 字符串。

用 Hutool 的版本(内容宽容):

java 复制代码
int discount = Convert.toInt(params.get("discount"), 0);
// "undefined" → 静默返回 0 → 用户全价下单 → 投诉 → 第二天才发现

用有界宽容的版本(内容严格):

java 复制代码
int discount = ValUtil.toInt(params.get("discount"), 0);
// "undefined" → NumberFormatException → 上线前测试就挂了

宽容是有代价的,代价由谁付------是开发者还是用户------这就是原则要回答的问题。

七、与既有原则的关系

原则 关系
Postel's Law(1981) 本原则是它在数据访问层的精化:保留"宽容接收",但加上"内容必须严格"。
Fail-Fast 在 Content 层一致,在 Missing 层反向:缺失不是错。
Tolerant Reader Pattern(Fowler) 互补:TRP 讲怎么读结构未知的数据,本原则讲读出来之后怎么取值。
Principle of Least Astonishment 是它的一个具体实例:getStr("不存在") 抛异常很惊人,toInt("abc") 静默返回 0 也很惊人。

八、给一个参考实现

我把这套原则实现在了开源库 dlz-kit 里,核心两个 API:

java 复制代码
// 取值(Missing-Lenient + Type-Lenient)
JSONMap data = new JSONMap(jsonStr);
String name = data.getStr("user.name");          // 不存在 → null
Integer age = data.getInt("user.age", 18);        // 不存在 → 18
Integer id  = data.getInt("user.id");             // "123" → 123(自动转)

// 转换(Type-Lenient + Content-Strict)
ValUtil.toInt("123");      // → 123
ValUtil.toInt(null);       // → null(不抛)
ValUtil.toInt("");         // → null(不抛)
ValUtil.toInt("abc");      // → NumberFormatException(抛!)

前两行帮你少写 10 行模板代码,最后一行帮你少修一个生产事故。

库只是参考实现,原则才是核心。你完全可以用任何语言、任何库实现它------只要对着第四节的判定表打勾就行。

九、把它带走

如果你只能记一句话:

对"没有"宽容,对"形不同"宽容,对"内容是垃圾"严格。

如果你只能记一张图:

scss 复制代码
        宽容 ←------------------------------|------------------------------→ 严格
   Missing       Type    │   Content
    (缺失)       (形式)   │    (内容)
                         ↑
                   这条线就是"有界"

如果你想引用这个原则:

Bounded Leniency Principle:在 Missing 与 Type 维度上宽容,在 Content 维度上严格。是 Postel's Law 在数据访问层的精化。


讨论:你见过的最离谱的"宽容/严格"翻车现场是什么?评论区聊聊。

如果这个原则帮到你,点个赞让更多人看到------一个有名字的原则,才能在 Code Review 里被引用。

相关推荐
浩风祭月4 小时前
让 AI 升级一个 4 年前的 React 项目:从 16 到 18 的完整记录
openai·ai编程
爱吃的小肥羊5 小时前
谷歌I/O解读:小模型反杀旗舰,3.5 Flash凭什么全面超越3.1 Pro?
aigc·ai编程
彦为君6 小时前
长时间运行的 Agent:如何设计可靠的执行框架
python·ai·ai编程
子昕6 小时前
看完 Google I/O 2026,我确信:多 Agent 时代不是概念了,Google 在造基础设施
ai编程
极品小學生6 小时前
拆解大模型时代的“流量交通枢纽”:API 中转站架构与核心原理
ai·架构·ai编程
uccs6 小时前
Agent循环原理
agent·ai编程·claude
货拉拉技术7 小时前
私域转化率翻倍的秘密:我们把多模态Agent融进了私域营销
人工智能·算法·设计模式
盼君7 小时前
AI生成了网页,怎么部署上线?从零到HTTPS全流程实录
ai编程
看山是山_Lau7 小时前
抽象工厂模式:一整套对象族如何统一创建?
设计模式·抽象工厂模式