Java 10 局部变量类型推断浅析

前言

java 10 引进一种新的闪闪发光的特性叫做局部变量类型推断。听起来很高大上吧?它是什么呢? 下面的两个情景是我们作为 Java 开发者认为 Java 比较难使用的地方。

上下文:陈词滥调和代码可读性

也许日复一日,你希望不再需要重复做一些事情。例如在下面的代码(使用 Java 9 的集合工厂),左边的类型也许会感觉到冗余和平淡。

import static java.util.Map.entry;
List<String> cities = List.of("Brussels", "Cardiff", "Cambridge")
Map<String, Integer> citiesPopulation
= Map.ofEntries(entry("Brussels", 1_139_000),
entry("Cardiff", 341_000));

这是一个非常简单的例子,不过它也印证了传统的 Java 哲学:你需要为所有包含的简单表达式定义静态类型。再让我们来看看有一些复杂的例子。举例来说,下面的代码建立了一个从字符串到词的柱状图。它使用 groupingBy 收集器将流聚合进 Map 。groupingBy 收集器还可以以一个分类函数为第一个参数建立映射的键和第二个收集器的 (counting()) 键计算关联的数量。下面就是例子:

String sentence = "A simple Java example that explores what Java
10 has to offer";
Collector<String, ?, Map<String, Long>> byOccurrence
= groupingBy(Function.identity(), counting());
Map<String, Long> wordFrequency
= Arrays.stream(sentence.split(" "))
.collect(byOccurrence);

复杂表达式提取到一个变量或方法来提升代码的可读性和重用性,这是非常有意义的。在这里例子中,建立柱状图的逻辑使用了收集器。不幸地是,来自 groupingBy 的结果类型几乎是不可读的!对于这一点你毫无办法,你能做的只有观察。

最重要的一点是当 Java 中增加新的类库的时候,他们开发越来越多的泛型,这就为开发者引进了更多的公式化代码(boilerplate code),从而带来了额外的压力。上面的例子并不是说明了编写类型就不好。很明显,强制将为变量和方法签名定义类型的操作执行为一种需要被尊重的协议,将有益于维护和理解。然而,为中间表达式声明类型也许会显得无用和冗余。

类型推断的历史

我们已经在 Java 历史上多次看到语言设计者添加“类型推断”来帮助我们编写更简洁的代码。类型推断是一种思想:编译器可以帮你推出静态类型,你不必自己指定它们。

最早从 Java 5 开始就引入了泛型方法,而泛型方法的参数可以通过上下文推导出来。比如

这段代码:

List<String> cs = Collections.<String>emptyList();

可以简化成:

List<String> cs = Collections.emptyList();

然后,在 Java 7 中,可以在表达式中省略类型参数,只要这些参数能通过上下文确定。比如:

Map<String, List<String>> myMap = new HashMap<String,List<String>>();

可以使用尖括号<>运算符简化成:

Map<User, List<String>> userChannels = new HashMap<>();

一般来说,编译器可以根据周围的上下文来推断类型。在这个示例中,从左侧可以推断出 HashMap 包含字符串列表。

从 Java 8 开始,像下面这样的 Lambda 表达式

Predicate<String> nameValidation = (String x) -> x.length() > 0;

可以省略类型,写成

Predicate<String> nameValidation = x -> x.length() > 0;

局部变量类型推断

随着类型越来越多,泛型参数有可能是另一个泛型,这种情况下类型推导可以增强可读性。Scala 和 C# 语言允许将局部变量的类型声明为 var,由编译器根据初始化语句来填补合适的类型。比如,前面对 userChannels 的声明可以写成这样:

var userChannels = new HashMap<User, List<String>>();

也可以是根据方法的返回值(这里返回列表)来推断:

var channels = lookupUserChannels("Tom");
channels.forEach(System.out::println);

这种思想称为局部变量类型推断,它已经在 Java 10 中引入!

例如下面的代码:

Path path = Paths.get("src/web.log");
try (Stream<String> lines = Files.lines(path)){
long warningCount
= lines
.filter(line -> line.contains("WARNING"))
.count();
System.out.println("Found " + warningCount + " warnings in the
log file");
} catch (IOException e) {
e.printStackTrace();
}

在 Java 10 中可以重构成这样:

var path = Paths.get("src/web.log");
try (var lines = Files.lines(path)){
var warningCount
= lines
.filter(line -> line.contains("WARNING"))
.count();
System.out.println("Found " + warningCount + " warnings in the
log file");
} catch (IOException e) {
e.printStackTrace();
}

上述代码中的每个表达式仍然是静态类型(即值的类型):

  • 局部变量 path 的类型是 Path
  • 变量 lines 的类型是 Stream<String>
  • 变量 warningCount 的类型是 long

也就是说,如果给这些变量赋予不同值则会失败。比如,像下面这样的二次赋值会造成编译错误:

var warningCount = 5;
warningCount = "6";
| Error:
| incompatible types: java.lang.String cannot be converted to int
| warningCount = "6"

然而还有一些关于类型推断的小问题;如果类 Car 和 Bike 都是 Vehicle 的子类,然后声明

var v = new Car();

这里声明的 v 的类型是 Car 还是 Vehicle?这种情况下很好解释,因为初始化器(这里是 Car)的类型非常明确。如果没有初始化器,就不能使用 var。稍后像这样赋值

v = new Bike();

会出错。换句话说,var 并不能完美地应用于多态代码。

那应该在哪里使用局部变量类型推断呢?

什么情况下局部类型推断会失效?你不能在字段和方法签名中使用它。它只能用于局部变量,比如下面的代码是不正确的:

public long process(var list) { }

不能在不明确初始化变量的情况下使用 var 声明局部变量。也就是说,不能使用 var 语法声明一个没有赋值的变量。下面这段代码

var x;

这会产生编译错误:

| Error:
| cannot infer type for local variable x
| (cannot use 'var' on variable without initializer)
| var x;
| ^----^

也不能把 var 声明的变量初始化为 null。实事上,在后期初始化之前它究竟是什么类型,这并不清楚。

| Error:
| cannot infer type for local variable x
| (variable initializer is 'null')
| var x = null;
| ^-----------^

不能在 Lambda 表达式中使用 var,因为它需要明确的目标类型。下面的赋值就是错的:

var x = () -> {}
| Error:
| cannot infer type for local variable x
| (lambda expression needs an explicit target-type)
| var x = () -> {};
| ^---------------^

但是,下面的赋值却是有效的,原因是等式右边确实有一个明确的初始化。

var list = new ArrayList<>();

这个列表的静态类型是什么?变量的类型被推导为 ArrayList<Object>,这完全失去了泛型的意义,所以你可能会想避免这种情况。

对无法表示的类型(Non-Denotable Types)进行推断

Java 中存在大量无法表示的类型——这些类型存在于程序中,但是却不能准确地写出其名称。比如匿名类就是典型的无法表示的类型,你可以在匿名类中添加字段和方法,但你没办法在 Java 代码中写出匿名类的名称。尖括号运算符不能用于匿名类,而var 受到的限制会稍微少一些,它可以支持一些无法表示的类型,详细点说就是匿名类和交叉类型。

var 关键字也能让我们更有效地使用匿名类,它可以引用那些不可描述的类型。一般来说是可以在匿名类中添加字段的,但是你不能在别的地方引用这些字段,因为它需要变量在赋值时指定类型的名称。比如下面这段代码就不能通过编译,因为 productInfo 的类型是 Object,你不能通过 Object 类型来访问 name 和 total 字段。

Object productInfo = new Object() {
String name = "Apple";
int total = 30;
};
System.out.println("name = " + productInfo.name + ", total = " +
productInfo.total);

使用 var 可以打破这个限制。把一个匿名类对象赋值给以 var 声明的局部变量时,它会推断出匿名类的类型,而不是把它当作其父类类型。因此,匿名类上声明的字段就可以引用到。

var productInfo = new Object() {
String name = "Apple";
int total = 30;
};
System.out.println("name = " + productInfo.name + ", total = " +
productInfo.total);

乍一看这只是语言中比较有趣的东西,并不会有太大用处。但在某些情况下它确实有用。比如你想返回一些值作为中间结果的时候。一般来说,你会为此创建并维护一个新的类,但只会在一个方法中使用它。在 Collectors.averagingDouble() 的实现中就因为这个原因,使用了一个 double 类型的小数组。

有了 var 之后我们就有了更好的处理办法 - 用匿名类来保存中间值。现在来思考一个例子,有一些产品,每个都有名称、库存和货币价值或价值。我们要计算计算每一项的总价(数量*价值)。这些是我们要将每个 Product 映射到其总价所需要的信息,但是为了让信息更有意义,还需要加入产品的名称。下面的示例描述了在 Java 10 中如何使用 var 来实现这一功能:

var products = List.of(
new Product(10, 3, "Apple"),
new Product(5, 2, "Banana"),
new Product(17, 5, "Pear"));
var productInfos = products
.stream()
.map(product -> new Object() {
String name = product.getName();
int total = product.getStock() * product.getValue();
})
.collect(toList());
productInfos.forEach(prod ->
System.out.println("name = " + prod.name + ", total = " +
prod.total));
This outputs:
name = Apple, total = 30
name = Banana, total = 10
name = Pear, total = 85

并非所有无法表示的类型都可以用 var - 它只支持匿名类和交叉类型。由通配符匹配的类型就不能被推断,这会避免与通配符相关的错误被报告给 Java 程序员。支持无法表示的类型的目的是在推断类型中尽量保留更多信息,让人们可以利用局部变量并更好地重构代码。这一特性的初衷并不是要人们像上面的示例中那样编写代码,而是为了使用 var 简化处理无法表示类型相关的一些问题。以后是否会使用 var 来处理无法表示的类型的一些细节问题,尚不可知。

类型推断建议

类型推断确实有助于快速编写 Java 代码,但是可读性如何呢?开发者大约会花 10 倍于写代码的时候来阅读代码,因此应该让代码更易读而不是更易写。var 对此带来的改善程度总是主观评价的,不可避免地会有人喜欢它,也会有人讨厌它。你应该关注的是如何帮助团队成员阅读你的代码,所以如果他们喜欢阅读使用 var 的代码,那就用,不然就不用。

有时候,显示类型也会降低可读性。比如,在循环遍历 Map 的 entryset 时,你需要找到 Map.Entry 对象的类型参数。这里有一个遍历 Map 的示例,这个 Map 将国家名称映射到其中的城市名称列表。

Map<String, List<String>> countryToCity = new HashMap<>();
// ...
for (Map.Entry<String, List<String>> citiesInCountry : countryToCity.entrySet()) {
List<String> cities = citiesInCountry.getValue();
// ...
}

然后用 var 来重写这段代码,减少重复和繁琐的东西:

var countryToCity = new HashMap<String, List<String>>();
// ...
for (var citiesInCountry : countryToCity.entrySet()) {
var cities = citiesInCountry.getValue();
// ...
}

这里不仅带来了可读性方面的优势,在改进和维护代码方面也带来了优势。如果我们在显式类型的代码中将城市从 String 表示的名称改为 City 类,以保留更多城市信息,那就需要重写所有依赖于特定类型的代码,比如:

Map<String, List<City>> countryToCity = new HashMap<>();
// ...
for (Map.Entry<String, List<City>> citiesInCountry : countryToCity.entrySet()) {
List<City> cities = citiesInCountry.getValue();
// ...
}

但使用了 var 关键字和类型推导,我们就只需要修改第一行代码就好:

var countryToCity = new HashMap<String, List<City>>();
// ...
for (var citiesInCountry : countryToCity.entrySet()) {
var cities = citiesInCountry.getValue();
// ...
}

这说明了一个使用 var 变量的重要原则:不要为了易于编码而优化,也不要为了易读而优化,而要了易维护性而优化。同时要考虑部分代码可能以后会修改而要折衷考虑代码的可读性。当然如果说添加类型推断对代码只会有好处略显武断,有时明确的类型有助于代码可读性。特别是当某些生成的表达式类型不是很直观时,我将选择显式而不是隐式类型,比如从下边的代码中我并不能看出 getCitiest() 方法会返回什么对象:

Map<String, List<City>> countryToCity = getCities();
var countryToCity = getCities();

既然要同时考虑到可读性和 var ,那么如何折衷就成了一个新问题,一个建议是:关注变量名,这很重要!因为 var 失去代码的易读性,看到这样的代码你根本不知道代码的意图是什么,这就使得起好一个变量名更加重要。理论上这是JAVA程序员应努力的方面之一,实际上许多 Java 代码可读性的问题根本不在语言的特性本身,而存在于一些变量的命名不太恰当上。

IDE 中的类型推断

许多 IDE 都有提取局部变量的功能,它们可以正确地推断出变量的类型,并为你写出来。这一特性与 Java 10 的 var 有一些重复。IDE 的这个特性和 var 一样都可以消除显式书写类型的必要性,但是它们在其它方面有一些不同。

局部提取功能会在代码中生成完整的、类型明确的局部变量。而 var 则是消除了在代码写显式书写类型的必要。所以虽然他们在简化书写代码方面有着类似的作用,但 var 对代码可读性的影响是局部提取功能所不具备的。就像我们前面提到,它多数时候会提高可读性,但有时候会可能会降低可读性。

与其它编程语言比较

Java 并不是首先实现变量类型推断的语言。类型推断在近几十年来被广泛应用于其它语言中。实际上,Java 10 中通过 var 带来的类型推断非常有限,形式上也相对拘束。这是一种简单的实现,可以将与 var 声明相关的编译错误限制在一条语句当中,因为 var 推断算法只需要计算赋值给变量的表达式的类型。此外,用在大多数语言中的 Hindley-Milner 类型推断算法在最坏的情况下会花费指数级时间,这会降低 javac 的速度。

总结

var 对于 Java 语言的生产力和可读性来说是一项很不错的新特性,但不应该止步于此。将来版本的 Java 将会继续保持语言的革新和现代性。举例来说,在 Java 10 发布仅仅 6 个月之后,就将发布并长期支持的 Java 11,其 var 关键字将可以在 lambda 表达式的参数中使用。这能让你拥有正式的参数类型推断能力,这很有用,不过,你还是需要加上 Java 注解,下面是例子:

(@Nonnull var x, var y) -> x.process(y)

一些函数式编程的想法已经被实现,并且已经为与将来的 Java 版本的结合做好准备。举例来说,模式匹配和值类型。这并不意味着 Java 会变得不再是我们熟悉和喜爱的 Java,它只是会变得比以前更灵活、可读性更强,并且更简洁。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详解关于Windows10 Java环境变量配置问题的解决办法

    关于Windows10 Java环境变量配置问题的解决办法 由于最近有一些时间,所以想要把之前学过一段时间的Java重新捡起来看看,之前的学习环境是Ubuntu,对于环境变量的配置和Windows也没有什么本质的区别,只不过是要用自带的编辑器更改一些东西而已. 那么我先讲讲我对于环境变量的一些自己的理解,由于每次编译源程序的时候需要用到编译工具,而Java的编译工具就是从oracle官网上下载的jdk包中的一些jar文件,所以如果要让系统识别java或者javac命令,那么就必须让系统知道这些文

  • win10设置java环境变量的方法

    1.首先,win10得找到设置的入口:Control Panel\All Control Panel Items\System 2.找到advanced system settings 以上这篇win10设置java环境变量的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • Java 10 局部变量类型推断浅析

    前言 java 10 引进一种新的闪闪发光的特性叫做局部变量类型推断.听起来很高大上吧?它是什么呢? 下面的两个情景是我们作为 Java 开发者认为 Java 比较难使用的地方. 上下文:陈词滥调和代码可读性 也许日复一日,你希望不再需要重复做一些事情.例如在下面的代码(使用 Java 9 的集合工厂),左边的类型也许会感觉到冗余和平淡. import static java.util.Map.entry; List<String> cities = List.of("Brussel

  • JDK10中的局部变量类型推断var

    Java是一种强类型, 许多流行的编程语言都已经支持局部变量类型推断,如js,Python,C++等 JDK10 可以使用var作为局部变量类型推断标识符 Local-Variable Type Inference(局部变量量类型推断),顾名思义只能用做为局部变量 注意 仅适用于局部变量量,如 增强for循环的索引,传统for循环局部变量不能使用于方法形参.构造函数形参.方法返回类型或任何其他类型的变量量声明标识符var不是关键字,而是一个保留类型名称,而且不支持类或接口叫var,也不符合命名规

  • 详解Java 10 var关键字和示例教程

    关键要点 Java 10引入了一个闪亮的新功能:局部变量类型推断.对于局部变量,现在可以使用特殊的保留类型名称"var"代替实际类型. 提供这个特性是为了增强Java语言,并将类型推断扩展到局部变量的声明上.这样可以减少板代码,同时仍然保留Java的编译时类型检查. 由于编译器需要通过检查赋值等式右侧(RHS)来推断var的实际类型,因此在某些情况下,这个特性具有局限性,例如在初始化Array和Stream的时候. 如何使用新的"var"来减少样板代码. 在本文中,

  • Java 10的10个新特性总结

    Java 9才发布几个月,很多玩意都没整明白,现在Java 10又要来了. 这时候我真想说:线上用的JDK 7,甚至JDK 6,而JDK 8 还没用熟,JDK 9 才发布不久不知道啥玩意,JDK 10-- 刚学Java的同学是不是感觉一脸蒙逼? 就连我这个老司机也同样感觉如此! Java 更新越来越快,我们做技术的也要跟上步伐,不然总会慢别人一拍,这新东西从国外到国内应用一般要好几年的时间,如果我们提前了解并应用这些新技术对自己不是坏事. Java 10的新特性 说了这么多,看Java 10都会

  • 浅析java 10中的var关键字用法

    2018年3月20日,Oracle发布java10.java10为java带来了很多新特性,其中让人眼前一亮的便是var关键字的引入. what •java10引入了局部变量折断 var用于声明局部变量. 如var user=new ArrayList<User>(); why •避免了信息冗余 •对齐了变量名 •更容易阅读 how •java10之前的变量声明: URL codefx = new URL("http://codefx.org") URLConnection

  • Java8新特性之泛型的目标类型推断_动力节点Java学院整理

    简单理解泛型 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.通俗点将就是"类型的变量".这种类型变量可以用在类.接口和方法的创建中. 理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作: List<Apple> box = new ArrayList<Apple>(); box.add(new Apple());Apple apple =box.ge

  • 浅析java中String类型中“==”与“equal”的区别

    一.前言 1.1.首先很多人都知道,String中用"=="比较的是地址,用equals比较的是内容,很多人对此用的是记忆法,通过记忆来加强此的引用,但是其真正的原理其实并不难,当我们真正明白其为什么的时候,用起来也会更加灵活,更加有底气(形容得不太好,朋友别见怪): 二相关知识的准备 类型常量池 运行时常量池 字符串常量池 我们今天讨论的主题是当然是字符串常量池: 为什么在这要把另外两个常量池拿出说一下呢,首先小生我在网上或者cnds上看到很多人在争论字符串常量池是存在与方法区还是堆

  • Java泛型之类型擦除实例详解

    目录 前言 泛型是什么? 泛型的定义和使用 泛型类 泛型方法 泛型类与泛型方法的共存现象 泛型接口 通配符 ? 无限定通配符 <?> <? extends T> 类型擦除 类型擦除带来的局限性 泛型中值得注意的地方 Java 不能创建具体类型的泛型数组 泛型,并不神奇 总结 前言 泛型,一个孤独的守门者. 大家可能会有疑问,我为什么叫做泛型是一个守门者.这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇.泛型是 Java 中一个很小巧的概念,但同时

  • 浅谈C#中的常量、类型推断和作用域

    一.常量常量是其值在使用过程中不会发生变化的变量.在声明和初始化变量时,在变量前面家关键字const,就可以把该变量指定为一个常量: const int a=100;//a的值将不可以改变 常量的特征: 1.常量必须在声明时初始化.指定了其值以后,就不能再修改了.2.常量的值必须能在编译时用于计算.因此不能从一个变量中提取的值来初始化常量.如果需要这么做,应该使用只读字段.3.常量总是静态的,但注意,不必在常量的声明中包含修饰符static.(实际上,不允许)在程序中使用常量至少有3个好处: 1

  • Java 8 动态类型语言Lambda表达式实现原理解析

    Java 8支持动态语言,看到了很酷的Lambda表达式,对一直以静态类型语言自居的Java,让人看到了Java虚拟机可以支持动态语言的目标. import java.util.function.Consumer; public class Lambda { public static void main(String[] args) { Consumer<String> c = s -> System.out.println(s); c.accept("hello lambd

随机推荐