程序员老鬼

某大厂P9,工作10年,年薪300万+,带上百人团队。他说:“今年业务砍了三轮,团队从200人砍到80人。下一个砍的,可能就是他

45岁,P9,干了10年,年薪三百多万,手底下以前两百号人,听着是不是挺稳?结果业务一砍就是三轮,团队从200砍到80,桌子还没凉呢,人已经少了一大半。

Image

最扎心的是,他不是看不懂局面的人。能做到这个位置,什么风向没见过?但真落到自己头上,照样慌。外面的人还觉得他是人生赢家,房子票子都有,履历也硬。

可公司算账的时候,不看你过去带过多少人,也不看你熬过多少夜。只看这条线还值不值钱,你这个成本还扛不扛得住。

所以他问“我还能撑多久”,其实挺真实的。到这个年纪,位置越高,反而越不好动。往上没坑,往下尴尬,换地方别人还得掂量你贵不贵。HR看完都得沉默两秒。

面试题:深度合并两个对象

线上配置一覆盖,老字段全没了。

明明只想把这一段:

{
"db": {
"pool": {
"max": 80
    }
  }
}

合到原配置里,结果最后只剩 max,min、timeout 全被干掉。

这种 Bug 我一看就不太想查业务代码,八成是合并对象的时候写成了“浅合并”。

比如原对象是这样:

{
"db": {
"host": "127.0.0.1",
"pool": {
"min": 10,
"max": 50,
"timeout": 3000
    }
  },
"log": {
"level": "INFO"
  }
}

新对象只传了:

{
"db": {
"pool": {
"max": 80
    }
  }
}

如果直接 putAll:

oldMap.putAll(newMap);

db 这一层会被整个替换掉。

也就是说,程序不会关心 db.pool.max 是不是只改了一个字段,它只看到 db 这个 key 被覆盖了。

这个地方坑就坑在,看代码像只改了一行,实际效果是删了一片。

深度合并要做的事也不复杂:两个对象的同一个 key,如果值都是对象,就继续往下合;否则右边覆盖左边。

我一般会把规则写死,不在业务里散着判断。

publicclassDeepMerger{

@SuppressWarnings("unchecked")
publicstatic Map<String, Object> merge(
            Map<String, Object> base,
            Map<String, Object> patch
    )
{
if (base == null) {
            base = new LinkedHashMap<>();
        }
if (patch == null || patch.isEmpty()) {
return base;
        }

for (Map.Entry<String, Object> item : patch.entrySet()) {
            String key = item.getKey();
            Object newVal = item.getValue();
            Object oldVal = base.get(key);

if (oldVal instanceof Map && newVal instanceof Map) {
                Map<String, Object> merged = merge(
new LinkedHashMap<>((Map<String, Object>) oldVal),
                        (Map<String, Object>) newVal
                );
                base.put(key, merged);
continue;
            }

            base.put(key, newVal);
        }

return base;
    }
}

这里我特意用了 LinkedHashMap,不是为了高级,就是为了排查问题时顺序别乱跳。

现场看日志的时候,字段顺序一变,人容易怀疑错方向。

跑一下:

Map<String, Object> oldCfg = new LinkedHashMap<>();
oldCfg.put("db", new LinkedHashMap<>(Map.of(
"host", "127.0.0.1",
"pool", new LinkedHashMap<>(Map.of(
"min", 10,
"max", 50,
"timeout", 3000
        ))
)));

Map<String, Object> newCfg = new LinkedHashMap<>();
newCfg.put("db", new LinkedHashMap<>(Map.of(
"pool", new LinkedHashMap<>(Map.of(
"max", 80
        ))
)));

Map<String, Object> result = DeepMerger.merge(oldCfg, newCfg);

System.out.println(result);

输出大概是:

{
  db={
    host=127.0.0.1,
    pool={
      min=10,
      max=80,
      timeout=3000
    }
  }
}

这才是“只改 max”。

不过这里还有个细节,null 到底算不算覆盖?

这个不能拍脑袋。配置中心里,我一般让 null 表示“清空字段”;接口参数里,我更倾向于忽略 null,因为前端少传字段太常见了。

如果业务希望 null 不覆盖,判断加一行就行:

if (newVal == null) {
continue;
}

别把这个规则藏在递归里面,后面排查会很难受。

数组也一样,别默认深度合并。数组到底是追加、去重,还是整体替换,业务差异很大。

我的习惯是:数组默认整体替换。

base.put(key, newVal);

简单,但不容易出脏数据。

深度合并这个题,难点不是递归,是边界规则。

同 key 都是对象,往下走。

不是对象,右边覆盖左边。

null 和数组,提前定规矩。

代码反而别写太花,越花越像给后人埋雷。