代码灵异事件:为什么用 get() 能取到值,换成 getProperty() 就变成了 null?

77 阅读4分钟

#Java #源码分析 #避坑指南 #面试题

摘要

明明向 Properties 中存入了数值,用 get() 探寻时它尚在,换作 getProperty() 却凭空消失,只返还一个冷冰冰的 null。 这并非 JDK 的 Bug,也不是什么玄学。本文将以此为引,拨开 java.util.Properties 的源码迷雾,一探这段因“继承滥用”而尘封已久的设计公案。

大家好,我是 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 方法是一个有着严重类型洁癖的方法:

  1. 它强制要求:Key 和 Value 必须都是 String 类型

  2. 在我们的案例中,存入的是 Integer 类型的 88

  3. oval instanceof String 判定为假。

  4. 所以变量 sval 被无情地赋值为 null

那为什么普通的 get() 方法能拿到呢?

因为它是个“老好人”。Properties 继承自 Hashtableget() 方法是父类的,签名是 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 指南

既然知道坑在哪,怎么避坑就很简单了。请严格遵守以下开发规范:

1. 拉黑 put 和 get

在操作 Properties 对象时,尽量不要使用这两个从父类继承来的方法。它们是给 Hashtable 用的,不是给你用的。

2. 锁死 setProperty 和 getProperty

JDK 专门提供了类型安全的方法,请一定要用它们:

// 正确姿势
props.setProperty("timeout", "88"); // 编译器会强制要求你传入 String

// 正确读取
String val = props.getProperty("timeout"); // 此时能正确获取 "88"

3. 巧用 defaults,让默认值成为你的神助攻

源码最后一行其实还藏了个彩蛋:

((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