升级 JDK 11 之后,代码里开始出现 var
2 月初我们把一个服务从 JDK 8u272 升到了 JDK 11.0.10。升级本身没什么好说的,倒是有个副作用:有同事开始用 var 了。
第一次在 review 里看到这段代码,我打了回退:
var result = orderService.queryOrderDetail(req);
var items = result.getItems();
for (var item : items) {
var price = item.getFinalPrice();
...
}
我说"看不出类型,可读性差"。他说"Java 10 就有的特性,官方都推荐"。谁也说服不了谁,于是我们花了半小时把所有 var 的用法过了一遍,最后定了一份团队规范。
先说明:var 是 Java 10(2018 年 3 月)引入的局部变量类型推断,不是 Java 11 的特性。只是我们用 JDK 8 一直没机会碰,升到 11 这个 LTS 才自然解锁。
什么情况下 var 确实更好
这几种写法我是接受的,甚至觉得比显式类型清爽:
// 1. 右侧已经写明了类型,左边重复一遍纯属冗余
var orderMap = new HashMap<Long, OrderDTO>(1024);
var requests = new ArrayList<OrderQueryRequest>(64);
// 2. 类型名极长,且变量名已经说明了它是什么
var ctx = new AbstractAnnotationConfigDispatcherServletInitializer() { ... };
var factory = new ConcurrentKafkaListenerContainerFactory<String, String>();
// 3. try-with-resources 里一次声明多个
try (var input = new FileInputStream(path);
var output = new BufferedOutputStream(new FileOutputStream(dest))) {
...
}
// 4. foreach 的循环变量,类型在集合声明里已经很明确
for (var entry : orderMap.entrySet()) {
...
}
第 2 种是官方 Style Guidelines for Local Variable Type Inference 里明确举的例子。这个文档是 Stuart Marks 写的,值得一看。
四个真实的坑
1. 右侧类型变了,编译器一声不响
这是最危险的一点。看这段代码:
var count = orderService.countByStatus(status); // 推断成 int
long total = count * pricePerItem;
某天 countByStatus 的返回值从 int 改成了 long(因为数据量涨了)。用显式 int 的话会编译报错,你会被迫检查这行代码;用 var 的话编译正常通过,行为静默地变了。
更隐蔽的是溢出场景:原来是 int 乘法溢出,改成 long 后不溢出了——听起来变好了,但这意味着原来依赖溢出行为的代码会出错(虽然这种代码本身就不该写)。
2. 推断出的类型和你想的不一样
var list = Collections.emptyList();
// 推断结果:List<Object>,不是 List<String>
list = someStringList; // 编译报错
var map = new HashMap<>();
// 推断结果:HashMap<Object, Object>,菱形符号失去意义
var x = 10;
// 推断结果:int。想写 long 必须写 10L
菱形运算符 <> 配合 var 时,泛型参数会被推断成 Object。这个是很多人第一次用就踩的。
3. lambda 和数组初始化不能直接用
var f = () -> System.out.println("hi");
// error: cannot infer type for local variable f
// (lambda expression needs an explicit target-type)
var arr = {1, 2, 3};
// error: cannot infer type for local variable arr
// (array initializer needs an explicit target-type)
var arr = new int[]{1, 2, 3}; // 这样可以
var f = (Runnable) () -> System.out.println("hi"); // 这样也可以
原因不难理解:lambda 表达式本身没有类型,它的类型由目标类型决定;var 又需要右侧来决定类型,两边互相依赖,推断不出来。
4. 通配符捕获类型会让变量变成"不可名状的类型"
这个坑比较深,但真会遇到:
List<? extends Order> orders = getOrders();
var first = orders.get(0); // 推断成 "capture of ? extends Order"
orders.set(0, first); // 编译报错!
// error: no suitable method found for set(int, capture#1 of ? extends Order)
用 var 时,编译器推断出的是捕获类型,这个类型你无法手写出来,一旦把它赋给 var 变量,后续能做的操作就受限了。改用 Order first = orders.get(0); 反而能过部分场景。
我们最后定的规范
讨论下来,写进团队 Code Style 的是这八条:
| 场景 | 结论 |
|---|---|
右侧用 new 且泛型参数已写明 | 推荐用 var |
| 类型名超过 40 字符或含多层泛型 | 可以用 var |
| try-with-resources 的多资源声明 | 可以用 var |
| foreach 循环变量 | 可以用 var |
| 右侧是方法调用,返回值类型非显而易见 | 不许用 var |
右侧是基本类型字面量(var x = 10) | 不许用 var |
配合菱形 new HashMap<>() | 不许用 var |
| 变量后续会被重新赋值 | 不许用 var |
最后一条的理由:var 声明的变量如果后面要赋别的值,你必须时刻记住它推断出的确切类型,这比写一次显式类型累多了。
开头那段被我打回的代码,最后改成了:
OrderDetailVO result = orderService.queryOrderDetail(req);
List<OrderItemVO> items = result.getItems();
for (OrderItemVO item : items) {
BigDecimal price = item.getFinalPrice();
...
}
多打几个字,但半年后回来看这段代码,不用猜 result 是什么。
一个折中办法
IDEA 有个功能挺好用:把光标放在 var 上按 Ctrl+Shift+P(Mac 是 Cmd+Shift+P)可以看推断出的具体类型。或者装个插件(比如 Java Var Inlay Hints)把推断类型直接显示在 var 后面。
但我不建议靠工具来弥补。代码是要被读的,如果读的人必须依赖 IDE 才能看懂,那就是代码的问题。
小结
var是 Java 10 的特性,只能用于局部变量,必须初始化,不能用于字段、方法参数、返回类型。var x = 10推断成int,var map = new HashMap<>()推断成HashMap<Object,Object>,这两个是高频误用。- 最大的风险是右侧返回类型变化时编译器不报错,行为静默改变。
- lambda 和数组初始化器不能直接用
var,需要显式转型或new int[]{}。 - 我们的规范:右侧是
new且类型明确时用,右侧是方法调用且类型不明时不用。
用不用 var 其实不是原则问题。判断标准就一条:不看等号右边,你能不能说出这个变量是什么类型?能就用,不能就老实写全。