JDK 9-17新功能30分钟详解-语法篇-var

介绍

JDK 10

JDK 10新增了新的关键字——var,官方文档说作用是:

大体意思就是用于带有初始化的局部变量声明,废话不多说,我们直接用具体代码来展示实际的作用。

List<String> listBefore10 = new ArrayList<>();   # 在JDK10之前
var listAfter10 = new ArrayList<String>();   # 在JDK10之后
listBefore10.add("9");
listAfter10.add("10");

JDK 11

JDK 11对var做了调整,允许var关键字用于Lambda函数里面的参数类型声明,示例:

var result = Arrays.asList("Java", "11").stream().reduce((var x, var y) -> x + y);
System.out.println(result.orElseThrow());

原理

可以看到使用了var关键字后,节省了一点声明内容,但是仔细一看,例如一个泛型类型从声明部分,挪到了初始化部分去了。我们直接看反编译后的class文件:
【原创】JDK 9-17新功能30分钟详解-语法篇-var-LMLPHP
可以看到,其实var关键字对于我们来说就是一个语法糖,编译完成后var声明的变量类型已经确定下来了,实际运行的时候是无法起到类似于Javascript语言var声明变量后还能动态更换类型的效果。至于为什么使用必须同时声明和初始化的方式,而不是先声明,后初始化再进行类型推断的方式,官方大体是基于下面考虑的

超过75%的JDK库及其相关扩展中,带有初始化的局部变量,都是有效不可变的,即使提供了延后初始化功能起到的作用也不大。

使用这种方式既能覆盖绝大数使用场景,又能保持功能简洁,另外一方面也是为了减少可能存在的维护问题,理解的心智成本,例如声明后经过几百行的代码再进行初始化。
具体内容感兴趣的可以看下JEPS 286的Scope Choices部分。

限制

1. 必须初始化

var原理大抵是编译器通过初始化的值推断声明的类型,由此引出使用它的一个约束——声明的同时必须进行初始化。

# 错误示例
var listAfter10;
listAfter10 = new ArrayList<String>();
listAfter10.add("10");

用以上代码直接编译运行,JDK会报错,提示:

如果使用IDE,都不用运行就会直接提示你,例如Intellij IDEA:
【原创】JDK 9-17新功能30分钟详解-语法篇-var-LMLPHP

回看之前说到的官方声明,“type inference to declarations of local variables with initializers”,with initializers已经很好说明使用它必须初始化,否则编译器无法进行类型推断。

2. 不能为null值

虽然进行初始化,但是使用null值的话,编译器仍然无法进行类型推断确定你最终的类型,也会报错。
【原创】JDK 9-17新功能30分钟详解-语法篇-var-LMLPHP

3. 不能用于非局部变量

回看之前说到的官方声明,“type inference to declarations of local variables with initializers”,local variable只能用于局部变量的使用,全局变量或者对象属性声明都不行,例如下面示例是无法正常运行:

# 错误示例
public class Java10 {

    public var field = "Not allow here";
}

编译直接报错

4. 不能用于Lambda表达式类型的声明

编译器不支持推断匿名函数的类型,例如:

# 错误示例
var lambdaVar = (String s) -> s != null && s.length() > 0;

【原创】JDK 9-17新功能30分钟详解-语法篇-var-LMLPHP

编译直接报错

但是这样使用是可以的:

# 正确示例
var lambdaVar = (Function<String, Boolean>) (String s) -> s != null && s.length() > 0;

不过这样写就是脱裤子放屁了,直接写在前面声明不是更好。
亦或者虽然使用了匿名函数,但是其返回值并不是一个Lambda表达式类型,也是可以的。

# 正确示例
var result = Arrays.asList("Java", "10").stream().reduce((x, y) -> x + y);

5. Lambda函数var修饰参数不能与其他类型混合使用

# 错误示例
var result = Arrays.asList("Java", "11").stream().reduce((var x, y) -> x + y);
System.out.println(result.orElseThrow());
# 错误示例
var result = Arrays.asList("Java", "11").stream().reduce((var x, String y) -> x + y);
System.out.println(result.orElseThrow());

就是同一个匿名方法里面要不就都是var修饰,要不就都不用,不能一个用,另外一个不用这种混合使用。当然官方说理论上是可行的,但是由于超出本次JEP规范定义,所以保留这些限制条件。

使用规范

使用var带来的好处是简化了开发者的局部变量声明成本,但是同时也可能造成代码维护上的不便,特别是开发者和维护者不是同一个人的情况,为此官方也出了一版7个小点的var使用规范

1. 使用有意义的变量名

# 不规范示例
List<Customer> x = dbconn.executeQuery(query);
# 正确示例
var custList = dbconn.executeQuery(query);

2. 局部变量使用范围尽可能地小

# 不规范示例
var items = new HashSet<Item>(...);
// ... 中间大概隔了几百行的代码 ...
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) {
   ...
}

一个方法行数过多,本身已经不利于维护,再加上使用var修饰变量,维护的人可能要鼠标滚动一屏甚至几屏才能看到var变量的具体使用,理解成本大大提高。所以一般情况下var变量保持在一屏内使用就好。

3. 初始化部分有意义时可以使用

var outputStream = new ByteArrayOutputStream();
var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c");

初始化的部分,例如调用的方法名称或者构造类型名字简单易懂,可以直接使用。

4. 用于拆分链式调用或者嵌套调用

return "test string".stream()
       .collect(groupingBy(s -> s, counting()))
       .entrySet()
       .stream()
       .max(Map.Entry.comparingByValue())
       .map(Map.Entry::getKey);

上面的链式调用不方便理解或者调试,可以改为

Map<String, Long> freqMap = "test string".stream()
                            .collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet().stream()
                                                .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

这种情况下可以进一步优化为

var freqMap = "test string".stream().collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet().stream().max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

5. 不用顾虑用于面向接口开发

List<String> normalList = new ArrayList<>();
var varList = new ArrayList<String>();  # varList最终推断类型是ArrayList<String>而不是List<String>

由于var只能用于局部变量,对于面向接口开发的原则基本无影响,问题主要是var初始化部分的类型依赖,如果发生变化,例如上面示例的ArrayList改成LinkedList,varList的类型随之变化。但是如果遵循规范“2. 局部变量使用范围尽可能地小”的话,影响面就会比较小。

6. 谨慎用于泛型类型

# 正确示例
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();
# 不规范示例
var itemQueue2 = new PriorityQueue<>();  # itemQueue2最终推断类型是PriorityQueue<Object>

可能导致类型推断的最终类型不是想要的泛型类型。

7. 谨慎用于字面量

byte flags = 0;
short mask = 0x7fff;
long base = 17;

改成

var flags = 0;
var mask = 0x7fff;
var base = 17;

全部类型都会推导为int。

08-17 14:20