Java 10 新特性之局部变量类型推断

2019-07-04 00:00:00 变量 局部 推断

前几天我在 如何评价 JDK 10 问题下的回答里对 Java 10 引入的局部变量类型推断特性进行了分析。不过在看过问题下的其他回答之后,我觉得很有必要把这部分内容单独拿出来写一篇文章来讨论。在本篇文章中,我会对 var 这个特性进行较为详尽的分析,并给出 var 的一些特殊用法。

在 Java 9 发布半年之后,Java 10 也在前几天正式发布了。Java 10 的所有新特性中,最为重要的一个特性就是局部变量类型推断。

让我们看看这份问卷的统计数据(感谢 @北南 如何评价 JDK 10 问题下的回答里给出的链接):

《Java 10 新特性之局部变量类型推断》
《Java 10 新特性之局部变量类型推断》

可以看到,调查中超过 80% 的人支持在 Java 中引入局部变量推断。而 JEP 266 中也说了:

Java is nearly the only popular statically typed language that has not embraced local-variable type inference; at this point, this should no longer be a controversial feature.

我觉得我们可以说 Java 10 最终引入这个特性是众望所归的。

Java 10 采用了一个叫做 var保留类型来实现局部变量推断。要特别注意的是,为了兼容旧版本,var 不是关键字,而是一个保留类型,也就意味着你仍然可以像这样用 var 为你的变量和函数命名:

int var = 10;

比较遗憾的是,Java 10 只引入了 var,而没有引入 Scala 和 Kotlin 中用于声明不可变变量的 val 关键字,而需要使用 final var 这种遗憾的语法来达到这个目的。不过考虑到 var 只是一个保留类型而不是关键字,Java 10 的这种做法似乎也不是不能理解。

var 可以在任何需要声明局部变量的地方,包括普通局部变量声明、for 和 twr 语句的声明部分的变量声明,以及增强型 for 循环中循环变量声明。现在我们可以写出这样的代码:

var i = 10;       // type of 'i' is int var str = "abc";  // type of 'str' is String var p = Paths.of("/home/glavo/java10.md"); // type of 'p' is Path try (var is = Files.newInputStream(p)) {   // type of 'is' is InputStream   // ... }

var list1 = new ArrayLlist<String>();    // type of 'list1' is ArrayList<String> var list2 = List.of("ice1000", "Glavo"); // type of 'list2' is List<String> 
var map = Map.of(1, "a", 2, "b"); // type of 'map' is Map<Integer, String> for(var entry : map.entrySet()) { // type of 'entry' is Map.Entry<Integer, String>   //... }

使用 var 声明的变量的类型和用于初始化它的表达式相同

请注意,var 的作用仅仅是推断变量类型,变量仍然是静态类型的,与 JS 中的 var 作用完全不同。下面这段代码无法通过编译:

var i = 10;
i = "20"; // error: incompatible types

使用 var 声明的变量时必须要在声明的同时初始化,所以下面这段代码是无法编译的:

var a; // error: 'var' on variable without initializer a = 10;

同时,var 也不能用于局部变量声明以外的地方(唯一的例外是 Java 11 会允许在 lambda 表达式的形式参数中使用 var 语法):

class C {
  var i = 10;  // error: 'var' is not allowed here   var f1() {   // error: 'var' is not allowed here     return 10;
  }
  void f2(var str) {  // error: 'var' is not allowed here     try {
      // ...     } catch (var e) { // error: 'var' is not allowed here       // ...     }
    System.out.println(str);
  }
}

var 还能用来声明一些不可指类型(Non-denotable types)的变量:

var obj = new Object() {  // type of 'obj' is A anonymous class types   int i = 10;
  void f() {}
};

var list = (List<String> & AutoCloseable) null; // type of 'list' is List<String> & AutoCloseable

不过直接用 null 初始化 var 声明的变量是不合法:

var a = null; // error: variable initializer is 'null'

这不难理解,虽然 null 具有一个独特的类型,不过这个类型的变量对于我们是没有意义的:它除了被赋值为 null 做不到其他的事情。

另外,配合使用 var 和菱形推断的时候也要注意,它们在一起可能会推断出在你意料之外的类型:

var l = new ArrayList<>(); // type of 'l' is ArrayList<Object>

刚才我们说过 var 可以用于声明不可指类型的变量。这个特性看起来用处不大,但实际上能够很好的为 Java 提供一些缺少的特性。

我们知道 Java 8 的 lambda 表达式不能够捕获可变变量,也就是说下面这个代码是错误的:

int count = 0;
List.of("ice1000", "Glavo")
    .forEach(e -> count += 1); //error
System.out.println(count);

之前想要绕过这个限制,我们可以用单元素的数组实现。而在 Java 10 中我们又多了一种选择:

var context = new Object() {
  int count = 0;
}
List.of("ice1000", "Glavo")
    .forEach(e -> context.count += 1);
System.out.println(context.count);

当有多个需要捕获的变量时,这种方式就要比使用数组简单的多,而这种方式其实就是 Scala 编译器实现捕获可变参数的方式。不过要注意的是,这样修改变量是线程不安全的。

除了帮助我们捕获可变参数,var 还能够帮助我们实现嵌套函数:

int factorial(int i) {
  var context = new Object() {
    int fact(int i, int accumulator) {
         if (i <= 1)
            return accumulator;
         else
            return fact(i - 1, i * accumulator);
      }
  };
  return context.fact(i, 1);
}

在使用长的 Stream 操作链的时候,我们也可以把一些操作放在 context 中,从而简化操作链,增强可读性:

int[] parseAndLogInts(List<String> list, int radix) {
  var context = new Object() {
    int parseAndLogInt(String str) {
      System.out.println(str);
      return Integer.parseInt(str, radix);
    }
  };
  return list.stream().mapToInt(context::parseAndLogInt).toArray();
}

另外,使用 var 我们能够在变量绑定的匿名类型里重写超类型中的方法。

在 Java 9 里我们可能会想着用这样的方法来实现类似 Golang 中的 defer 语句:

AutoCloseable context = () -> { /* ... *. };
try (context) {
    // ... }

不过很遗憾,这样写是无法通过编译的。因为 AutoCloseable 接口的 close 方法声明为可能抛出任何异常,所以你必须要用 catch 捕获全部异常,这可不是我们想要的。不过在 Java 10 里,我们能用匿名类型来重写掉 AutoCloseable 中的异常声明,从而避免被要求强制捕获异常。我们把 parseAndLogInts 方法来改写一下,让它在返回后打印出字符串 “exit”:

int[] parseAndLogInts(List<String> list, int radix) {
  var context = new AutoCloseable() {
    int parseAndLogInt(String str) {
      System.out.println(str);
      return Integer.parseInt(str, radix);
    }
    public void close() {
      System.out.println("exit");
    }
  };
  try(context) {
    return list.stream().mapToInt(context::parseAndLogInt).toArray();
  }
}

最后我再来讨论一下这几天在网上看到的一些观点:

Java 是在学 JavaScript

JavaScript 已经抛弃了 var

这一点前面已经解释过了,JavaScript 和 Java 的 var 并不是一种东西。Java 的 var 是参考了 Scala、Kotlin、C#、Swift、C++ 以及 Go 等语言最后实现的,是静态类型的类型推断,而非动态类型。

var 会影响到类库的接口

实际上 var 只能在局部使用,所以对类库的接口不会造成影响。

var 影响可读性

这个观点在某些范围内确实可能成立,但是如果能规范使用 var ,不仅不会让代码难以阅读,反而还会提升可读性,特别是对于在 IDE 之外阅读和写 Java 代码的情况非常友好。

很多人之所以持有这个观点,是因为他们认为使用 var 需要人们自行推断变量的类型。但是,很多情况下我们的代码会是这样:

List<String> nameList = List.of("Glavo", "ice1000");
Map<String, List<String>> map = new HashMap<>();
Path p = Paths.get("/", "home", "glavo", "xxx.txt");
String name = "Glavo";
int count = 0;
try(InputStream is = Files.newInputStream(p)) {
    // ... }
Stream<String> nameStream = nameList.stream();
for(Map.Entry<String, List<String>> e : map.entrySet()) {
    // ... }
// ... String ans = nameList.toString();

实际上,我们能够注意到,很多变量的类型都是能够直接看出的。new 表达式、字面量、工厂方法、初始化表达式最终调用的方法名以及变量名本身都是包含着类型信息的,更多的类型信息不仅仅无助于阅读,更增加了语法噪声,给阅读带来了障碍。在 IDE 内阅读的难度或许还可以接受,但是如果要在 IDE 之外阅读,那对于视觉的影响就太大了。

IDEA 里查看这段代码:

《Java 10 新特性之局部变量类型推断》
《Java 10 新特性之局部变量类型推断》

放到 IDE 之外:

《Java 10 新特性之局部变量类型推断》
《Java 10 新特性之局部变量类型推断》

关掉高亮:

《Java 10 新特性之局部变量类型推断》
《Java 10 新特性之局部变量类型推断》

即使是 IDE 里,我都难以一眼找到 map 这个变量名,而后的就近乎灾难了。

如果把这几段代码换用 var 会怎么样呢?

IDE 内:

《Java 10 新特性之局部变量类型推断》
《Java 10 新特性之局部变量类型推断》

文本编辑器内:

《Java 10 新特性之局部变量类型推断》
《Java 10 新特性之局部变量类型推断》

关掉高亮:

《Java 10 新特性之局部变量类型推断》
《Java 10 新特性之局部变量类型推断》

可以明显的看出来,在这种情况下,使用 var 对可读性有明显的提升。

当然,并不是任何时候使用 var 都能这样清晰的看出类型。如果 var 被用于声明一个被复杂、难以看出类型的表达式初始化的情况,这种情况下 var 可能会导致使用文本编辑器阅读代码难度增加(IDE 内一般都有显示类型的快捷键,而且 IDEA 也有 @郑小信 开发的 JavaX Var Type Hint 的插件,对阅读难度上的影响应该不会太大),所以我们需要使用规范来约束 var 的使用,以此规避 var 造成的问题。

//TODO: 过几天我会把 Style Guidelines for Local Variable Type Inference in Java 翻译一下给大家做参考

    原文作者:Glavo
    原文地址: https://zhuanlan.zhihu.com/p/34911982
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章