2057 字
约 6 分钟
14
S04-A22 · `var` 的边界:它省的是"类型声明",不是"类型思考"

一段代码把 Map<String, List<Order>> orders = load(); 改成了 var orders = load();,看起来更简洁。三个月后新人问"这到底是 Map 还是 List"——他得跳到 load() 的定义才能知道。而 var 的初衷是"省略冗余的类型",不是"省略需要知道的类型。

var(Java 10)是"局部变量类型推断",它有三条硬边界与一条软边界——而后者(可读性)才是"该不该用"的真正判据。


1. 三条硬边界

边界 说明
只能用于局部变量 字段/参数/返回值不行
必须有初始化表达式 var x; 编译失败
推断为"编译期的具体类型" 赋 null 不行;var x = null 失败

第三项很实用:**var list = new ArrayList<String>() 推断出的类型是 ArrayList<String>**第三项的"后果"很具体:接口上的方法(如 List.of 的 add 不支持)会"在你手里变成可调用的"——于是"编译通过,运行抛异常",而这正是 List 与 ArrayList 的语义差。
,而不是 List<String>——于是"后面只能调 ArrayList 的方法"。

// 三种"推断结果"的对照
var a = new ArrayList<String>();     // ArrayList<String>
var b = List.of("x");                // List<String>(工厂方法返回接口)
var c = 1;                           // int(不是 Integer)
// 差异:a 可以 add,b 不行;c 是基本类型

2. "省了类型"之后"读者少了什么"

信息 是否丢失
变量类型 丢失(要跳去看右边)
泛型参数 可能丢失(右边常省略)
语义(它是什么业务概念) 与 var 无关(靠命名)

第二项最容易被忽略:右边常写成 new HashMap<>(),于是"泛型参数"完全看不出——而 var map = new HashMap<String, Order>() 反而更清楚。

3. 三种"该用 var"的情形

情形 理由
右边已显式写了类型(new Foo(...)) 左侧冗余
右边是"工厂/构造"且类型清晰(List.of(...)) 无歧义
类型名很长且"不影响理解"(Map<String, List<Order>>) 减少噪声

这个"初衷"值得记住:var 的语言设计目标就是"消除冗余",而不是"隐藏类型"——所以"隐藏了类型"的用法,本身就是"超出设计意图"。
第一项的"边界"在"构造参数不显示类型"时:var x = new Foo(bar) 仍能看出是 Foo——所以"泛型式构造"(new HashMap<>())是唯一"看不出"的例外。
第一项是"教科书场景":Foo foo = new Foo(); 的类型写了两遍,var 去掉一遍。

4. 三种"不该用 var"的情形

情形 后果
右边不显示类型(load()、get()) 读者要跳转
类型是"接口/实现"的关键信息 丢失抽象意图
后面紧跟"链式调用"且类型影响行为 误判

第一项是"最常犯"的:var result = service.query(cmd)——而"result 是 List 还是 Page"决定了"能不能直接遍历"。第一项的"修法"有两种:写显式类型,或给变量起一个"带类型线索"的名字(page vs list)——后者不改签名,但要求"命名能承载类型信息"。

第三项在"Stream/集合"上很实际:var x = list.stream() 得到 Stream<T>,而很多人以为是 List。

"三条 var 规则"可以写进团队的代码约定,各带一个例子:

// 团队的 var 约定(三条,都可用"示例"判定)
1 右侧显式构造 → 用 var:var cache = new CaffeineCache<>(spec);
2 右侧是方法调用 → 写显式类型:List<Order> orders = repo.findByUser(uid);
3 循环变量 → 用 var:for (var e : entries) { ... }
// 判据:评审时"引用规则编号",而不是"我觉得"

5. "可读性判据"怎么写

判据 用法
一眼能看出类型 用 var
需要跳转才能知道 写显式类型
类型名是"抽象意图"(List vs ArrayList) 写显式类型

第三项的"量化"是"左侧类型出现在公开签名里的频率":接口类型是"契约",会在方法签名上重复出现——于是"左侧写接口"不是冗余,而是"契约的可见化"。
第三项是"设计"层面的:"我用 List 是因为我不关心具体实现"——而这个"不关心"本身是"给读者的信息",var 会把它抹掉。

// 同一段逻辑的三种写法(可读性递减)
List<Order> orders = repo.findByUser(uid);        // 清楚:接口类型 = 意图
var orders = repo.findByUser(uid);                // 省事:要跳去看返回类型
var orders = new ArrayList<Order>(); repo.fill(orders, uid);   // 最差:实现类型暴露

"推断结果"可以直接用一个测试"钉住",避免"以为它是接口":

// 一个"钉类型"的测试(编译期就能发现改变)
@Test void var_infers_concrete_type() {
    var list = new ArrayList<String>();
    list.add("x");                                  // 只有实现类能 add
    assertEquals(1, list.size());
}
// 用途:改实现(ArrayList → LinkedList)时,这个测试会提醒"行为可能不同"

6. 排错:五种"var 相关问题"

现象 成因 处理方向
编译报"cannot infer" 无初始化/null 写显式类型
后面调不到方法 推断成实现类 左侧声明接口类型
泛型丢参数 右边用了 <> 显式写泛型
新人读不懂 右边不显示类型 改显式
误以为某类型 stream() 之类 改显式或加命名

第二项的表现是"编译错误"(不是运行错误),所以它"不会静默"——但它会让你"改回显式类型",而那是 var 在这个位置"本不该用"的证据。

"迁移到 var"应当"局部、逐步",而不是"一次全改":

// 三步迁移(每步可回滚)
1 只在"新代码"里按约定用 var(不动存量)
2 存量按"文件"逐个改,每个 PR 只改一个文件
3 评审规则进 linter(如禁止"右侧是方法调用时用 var")
// 反例:一次全仓库批量替换 → diff 巨大、评审失效、回滚困难
// 补充:迁移时"类型信息丢失"的代价,往往在"半年后新人接手"才显现——所以它值得"慢一点"

7. 边界:什么情况无所谓

场景 为什么 做法
一次性脚本 无长期读者 随意
极短作用域(下一行就用完) 无阅读负担 可 var
循环变量 类型来自集合 可 var
团队统一约定 一致性优先 跟随约定

第三项的"原因"是"类型来自"被遍历的集合",而集合的类型就在上一行"——于是"不跳转也能知道",这是"最理想"的 var 场景。
第三项是"最安全"的 var 用法:for (var item : items)——类型由 items 决定,读者不必跳转。

判断口径一句话:"读者不看右边,能不能知道这个变量是什么类型"。最后一条与"团队"有关的边界:var 需要一个"团队约定"(如"仅在右侧显式构造时用")——否则同一个人在不同文件里用不同风格,评审会反复争这个。
能(new Foo()、for (var x : list)),用 var 更干净;不能(load()、query()),写显式类型——因为"省下的那 15 个字符"换来的是"每次阅读都要跳转"。


8. 自测

  1. 三条硬边界是什么?为什么 var x = null 编译失败?
  2. var list = new ArrayList<String>() 推断出什么?为什么它"只能调 ArrayList 的方法"?
  3. "省了类型"之后读者少了哪些信息?泛型参数为什么会丢?
  4. 哪三种情形"该用 var"?哪三种"不该用"?
  5. 为什么"我用 List 是因为我不关心具体实现"本身是"给读者的信息"?
  6. 判断"该不该用 var"的那个问题是什么?
S04-A22 · `var` 的边界:它省的是"类型声明",不是"类型思考"
https://www.clxhxhhr.top/posts/6625/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。