侦探大挑战
128.12M · 2026-04-22
#Java #源码分析 #避坑指南 #面试题
大家好,我是 Re-Zero。
今天在 Code Review,我见到了一段足以写入《Java 迷惑行为大赏》的代码:某位兄弟图省事,直接用 put 方法把数字往 Properties 对象里塞,取的时候却遭遇“左右手互搏”——一只手能拿到,另一只手却摸了个空。
咱们来还原一下这个让新手摸不着头脑的“灵异现场”。
我们写一段最简单的测试代码:
Properties props = new Properties();
// 1. 注意:为了省事,这里直接存了个整数 88
// 这里的 88 是 Integer 类型,不是 String
props.put("timeout", 88);
// 2. 使用 Map 原生的 get() 方法
System.out.println("get: " + props.get("timeout"));
// 3. 使用 Properties 专属的 getProperty() 方法
System.out.println("getProperty: " + props.getProperty("timeout"));
按照正常逻辑,这就好比你往钱包里塞了一张钱,用左手掏出来是钱,用右手掏出来应该是空气吗?显然不合理。
但 Java 偏偏就给你变了个魔术,运行结果如下:
get: 88
getProperty: null <-- 见鬼了?!
明明数据就在里面,用 get 都能拿到,凭什么 getProperty 就视而不见?难道它还歧视整数?
为了搞清楚原因,我们直接点开 java.util.Properties 的源码,看看 getProperty 到底背着我们干了什么。
代码非常短,但每一行都藏着心眼:
public String getProperty(String key) {
// 1. 先调用父类 (Hashtable) 的 get 方法,拿到 value
Object oval = super.get(key);
// 2. 关键判决!
// 检查拿到的 value 是不是 String 类型
// 如果是 String,强转;如果不是,sval 直接置为 null
String sval = (oval instanceof String) ? (String)oval : null;
// 3. 兜底逻辑:如果是 null,且存在默认配置集 (defaults),则去默认集里找
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}
破案了,凶手就是这行代码:
String sval = (oval instanceof String) ? (String)oval : null;
getProperty 方法是一个有着严重类型洁癖的方法:
它强制要求:Key 和 Value 必须都是 String 类型。
在我们的案例中,存入的是 Integer 类型的 88。
oval instanceof String 判定为假。
所以变量 sval 被无情地赋值为 null。
那为什么普通的 get() 方法能拿到呢?
因为它是个“老好人”。Properties 继承自 Hashtable,get() 方法是父类的,签名是 Object get(Object key)。在它眼里,众生平等,不管你存的是字符串还是整数,它都原样给你吐出来。
你可能会问:既然 Properties 是专门用来处理字符串配置文件的,为什么 Java 还要允许我们往里面 put 整数呢?直接报错不好吗?
这就得聊聊 Java 早期的“黑历史”了。
Properties 类出现得非常早(JDK 1.0)。在那个上古时代,Java 的设计者为了少写几行代码,强行让 Properties 继承 了 Hashtable。
public class Properties extends Hashtable<Object,Object> { ... }
这就好比让一个文弱书生(Properties),直接继承了屠夫(Hashtable)的技能树。虽然书生的本职工作是写字,但他确实从他爹那里继承了“杀猪”的本事——虽然能用,但怎么看怎么别扭。
这是一个典型的“组合优于继承”原则的反面教材。如果当时采用组合模式(Properties 内部持有一个 Map),就不会暴露 put 方法给用户,也就不会有今天这个坑了。
既然知道坑在哪,怎么避坑就很简单了。请严格遵守以下开发规范:
在操作 Properties 对象时,尽量不要使用这两个从父类继承来的方法。它们是给 Hashtable 用的,不是给你用的。
JDK 专门提供了类型安全的方法,请一定要用它们:
// 正确姿势
props.setProperty("timeout", "88"); // 编译器会强制要求你传入 String
// 正确读取
String val = props.getProperty("timeout"); // 此时能正确获取 "88"
源码最后一行其实还藏了个彩蛋:
((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
这允许我们构建一个“配置层级链”。比如你可以有一个 defaultProps 存放默认配置,然后创建一个 appProps 覆盖它。当主配置里没值时,它会自动去默认配置里找,非常方便。
Properties defaultProps = new Properties();
defaultProps.setProperty("theme", "light"); // 默认浅色主题
// 将 defaultProps 设为 appProps 的“父级”
Properties appProps = new Properties(defaultProps);
// appProps 里没配 theme,getProperty 会自动去 defaultProps 里找
System.out.println(appProps.getProperty("theme")); // 输出 "light"
下次再遇到 getProperty 返回 null,先别怀疑人生,检查一下:你是不是手滑用了 put 存了非 String 类型的数据?
| 方法 | 来源 | 特点 | 推荐指数 |
|---|---|---|---|
| get() | Hashtable | 返回 Object,来者不拒,容易埋雷 | 禁止使用 |
| getProperty() | Properties | 只返回 String,带默认值查找,类型安全 | 强烈推荐 |
既然用了 Properties,就请尊重它的“洁癖”,只喂给它 String!