Administrator
发布于 2021-02-06 / 4529 阅读
93

var 局部变量类型推断:用还是不用

升级 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 的用法过了一遍,最后定了一份团队规范。

先说明:varJava 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 推断成 intvar map = new HashMap<>() 推断成 HashMap<Object,Object>,这两个是高频误用。
  • 最大的风险是右侧返回类型变化时编译器不报错,行为静默改变。
  • lambda 和数组初始化器不能直接用 var,需要显式转型或 new int[]{}
  • 我们的规范:右侧是 new 且类型明确时用,右侧是方法调用且类型不明时不用。

用不用 var 其实不是原则问题。判断标准就一条:不看等号右边,你能不能说出这个变量是什么类型?能就用,不能就老实写全。

参考