一段代码把 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. 自测
- 三条硬边界是什么?为什么
var x = null编译失败? var list = new ArrayList<String>()推断出什么?为什么它"只能调 ArrayList 的方法"?- "省了类型"之后读者少了哪些信息?泛型参数为什么会丢?
- 哪三种情形"该用 var"?哪三种"不该用"?
- 为什么"我用 List 是因为我不关心具体实现"本身是"给读者的信息"?
- 判断"该不该用 var"的那个问题是什么?