Java 10 - 与“var类型推断机制”的第一次亲密接触

3,127 阅读12分钟

这里写图片描述

引言

官方消息,Java 10 将在2018年3月20号正式发布。(我大Java 9 瞬间成了Vista..........)据传,官方在2017年12月14号已经完成了版本开发的主线合并。 迄今为止,在官方放出了Java 10少数新特性里面,局部变量类型推断(local-variable type inference) 绝对是备受万众瞩目的。它将我们常常在JS里面使用的var 变量引入到语言特性中,把我们从那些冗长的变量声明中解放出来。来吧,舒展你的右手,下面是你以前绝对没有写过的代码:

var users = new ArrayList<User>();

还是禁不住要感叹Java的伟大,“集大广成”,没有什么词语能更好地形容了。So,看到这样的代码我猜你一定很有兴趣想知道更多关于它的信息,这篇文章将讨论var 适用于哪里,它是如何影响可读性的以及在类型推断的过程中发生了什么。

用var替换传统变量声明

作为一名有着麒麟臂的Java开发者,我们日撸月撸的Java是一门强类型语言,也就是要显式声明各个变量的确切类型,稍有不慎编译器就会报错。如以下代码,写一行代码要输入两次类型,一次是引用类型声明,另一次是构造函数:

URL lovnx = new URL("https://github.com/Lovnx");

我们还经常为下一行代码声明变量类型:

URL lovnx = new URL("https://github.com/Lovnx");
URLConnection connection = lovnx.openConnection();
Reader reader = new BufferedReader(
	    new InputStreamReader(connection.getInputStream())
    );

正是由于这样的原因,我的基友某獒十分鄙夷Java,要不是想学Java的并发处理,这厮估计得一辈子不待见Java。上面这种情况还不是最可怕的,尽管看起来是有些多余。我们的IDE会帮助我们更好地书写代码,好用的快捷键,以及自动提示。但是当我们的变量名跳跃性较大的时候,可读性会受到极大的影响,因为当变量类型的字符数参差不齐,或者声明一个中间变量的时候,往往会使人感到心力交瘁,专注度不能获得正向的反馈,写了半天也瞧不出有几个逻辑。这种语言特性,饱受诟病,尤其是被Python开发者。。。

从Java 10开始,开发者就可选择通过把变量声明为var 来让编译器自行推断其类型:

var lovnx = new URL("https://github.com/Lovnx");
var connection = lovnx.openConnection();
var reader = new BufferedReader(
    new InputStreamReader(connection.getInputStream()));

编译器在处理var 变量的时候,它会去检测右侧代码的声明,并将其类型用于左侧,这一过程发生在初始化阶段。WTF?甜到忧伤?JIT在编译成字节码的时候还是用的推断后的结果类型。

正如你所看到的,这样在键入代码的时候可以节省不少字符,更重要的是,可以去除冗余的信息,使代码变得清爽,还可以对齐变量的名称。当然这样也会付出一些代价,一些变量,比如上文的connection 不是由构造函数直接创建,我们相较以往将不会立刻知道它的实际类型,只能借助IDE了。

另外,如果你担心命名成var 的方法与变量冲突,不用担心,从技术上来讲,var 不是一个关键字,而是一个保留的类型名称,也就是说它的作用域只在编译器推断类型的范围内,而在其他地方还是有效的标识符。这样也限制了一些东西,即类名不能起为var

局部变量类型推断看起来像一个简单的语言特性,但实际上并不简单,可能在你的心中已经有了一些疑问:

  • 这货到底是 Java 还是 JavaScript?
  • 该在什么地方使用?
  • 它真的不会破坏以往强类型的可读性?
  • 为什么又没有 val 或者 let?

往下看,你会得到答案。

它不是JavaScript

var 丝毫不会影响Java对一个静态类型的归类,它做的,仅仅是在编译器中推断出变量类型,然后将实际的类型写入字节码,就像以往的强类型显式声明一样。

举个例子,以下代码就是对上面那段有var 代码的字节码的反编译结果:

URL lovnx = new URL("https://github.com/Lovnx");
URLConnection connection = lovnx.openConnection();
Reader reader = new BufferedReader(
	    new InputStreamReader(connection.getInputStream())
    );

事实上,var 的生命周期只存在于编译器当中,并没有针对它涉及的运行时组件,所以大可放心没有性能影响。所以,它并不是JavaScript那样把它当成一个关键字来解析,没有人能够一蹴而就。

如果你担心没有明确的类型会使代码变得不好阅读,那么你在写函数式语句的时候就压根不会有一个变量:

rhetoricalQuestion.answer(yes -> "see my point?");

var的适用范围

JEP 286’的标题是“局部变量类型推断”, 看名称就可以知道使用范围:局部变量。更加确切的说法是:具有初始化器的局部类型变量声明。所以下面这种方式是不行的:

//nope
var foo;
foo = "Foo";

必须得是 var foo = "Foo"。上面的例子没有涵盖所有不能使用var 的聚集表达式。比如lambdas和方法引用,编译器会根据预期的类型确定类型,下面这些情况也不行:

//nope
var ints = {0, 1, 2};
var appendSpace = a -> a + " ";
var compareString = String::compareTo

除了局部变量之外,还有一个用处是用在for 循环里面:

//right
var numbers = List.of("a", "b", "c");
for (var nr : numbers)
    System.out.print(nr + " ");
for (var i = 0; i < numbers.size(); i++)
    System.out.print(numbers.get(i) + " ");

这表示,字段、方法签名、catch子句仍然需要显示类型声明,下面这种是错误的:

// nope
private var getFoo() {
    return "foo";
}

避免一些莫名其妙的错误

var 只能用于局部变量并不是JDK团队技术上的局限,这还是Java语言特性所决定的。就像下面这样:

// cross fingers that compiler infers List<User>
var users = new ArrayList<User>();
// but it doesn't, so this is a compile error:
users = new LinkedList<>();

编译器肯定是能够轻易得知代码情况的,并可以轻易地推断出所有的类型,但事实上没有这样做。JDK团队想尽量让使用者避免一下莫名其妙的错误, 不应该改掉一个地方而导致一个看似不相干的错误。

下面是一个例子:

// inferred as `int`
var id = 123;
if (id < 100) {
    // very long branch; unfortunately
    // not its own method call
} else {
    // oh boy, much more code...
}

上面的代码没有任何问题,现在我们在条件体里面追加这一行代码:

id = "124"

会发生什么?这不是一个浮夸的问题,想一想。

答案是if 条件会抛出一个错误,因为id 不再是一个int 型的变量,不能和< 进行比较。这个错误与造成这种错误的原因相去甚远。这显然是给一个变量赋值而无法预料到的结果。

从这个角度来看,将类型推断限制在JIT中即时类型的决定是有道理的。

为什么不能用于声明类属性与方法返回值类型?

属性与方法的作用域比局部变量大得多,因此稍有不慎,就有可能出现上文那种毫无征兆的错误。在最坏的情况下,更改方法参数的类型可能引起序列化二进制不兼容,从而导致运行时错误。这就是改变一些极小细节而带来的极端后果,并且是毫无征兆的。

因此,由于非private的属性和方法是类的静态组成部分,不允许被瞎改,所以类型推断就舍弃了它们。当然,对private的属性与方法理论上是可以使用类型推断的,但如果强行+1,不免显得有点奇怪。

归根结底其基本原因,还是局部变量只是一些实现细节,不能被外部引用,而这就减少了其严格,明确和详细地定义(强类型)其类型的需要。(我看是偷懒吧===)

Java 10 引入var的背景

让我们从后文找出引入var 类型推断的的原因,以及它是如何影响代码可读性的,为什么vallet 没有随之一起引入。如果你还是对其他细节颇有兴趣,可以参照官方JEP 286, var FAQ问题解答,或者是Amber项目的邮件列表。

But why?!

Java的语法历来以冗长著称,尤其是对比一些年轻的语言,这以及成为开发者最大的痛点之一,你往往会听到一些初学者与高级开发者的诟病与抱怨。Project Amber, var 的原始项目, 致力于孵化出一种“体积更小,面向生产效率”的Java新语言特性,减少一些以往过于累赘的语法规则。

如此,局部变量类型推断机制(Local-variable type inference)便应运而生了。在编写代码的时候,可以很明显地使变量的声明变得简洁,虽然到目前为止我仍认为它与IDE的自动生成功能相比是喜忧参半的,比如在重写过程中,或者写一个构造方法,抑或是为方法的返回值声明一个类型。

var的好处除了使局部变量的声明更加简便之外,还能使代码相得益彰,why?如果你曾经或现在致力于过企业级开发,你会觉得那些命名相当丑陋。下面就是一个典型栗子:

InternationalCustomerOrderProcessor<AnonymousCustomer, SimpleOrder<Book>> orderProcessor = createInternationalOrderProcessor(customer, order);

它就像王大娘的裹脚一般,又臭又长,一个很简单的功能要写到吐血,中间还夹叙夹议才能保证语义明确,避免后期维护的时候看不懂写的什么。

var orderProcessor = createInternationalOrderProcessor(customer, order);

以往声明中间变量的爱恨纠葛,在引入了var之后可以彻底地冰释前嫌了,现在你在方法体内可以什么也不管,一路var下去,特别是一些嵌套的或者连锁表达式,它的好处也更加显而易见。

简而言之,var就是减少Java累赘语法的一颗语法糖,谁先尝到谁先甜到忧伤。

And What About 可读性?

现在来看看可读性的影响。毫无疑问,使用var势必会引起变量类型可视化缺失, 这会伤害一部分的可读性,特别是当你想要知道一些代码的运行逻辑的时候,能够目所能及地看到变量类型显得格外重要,尽管将来的IDE可能会智能显示所有推断类型,这是当前唯一可能会受到批评的地方。

针对可读性缺失,var从其他地方来弥补,其中一种方式就是使变量名称对其(呵呵00好像是好看了一些):

// with explicit types
No no = new No();
AmountIncrease<BigDecimal> more = new BigDecimalAmountIncrease();
HorizontalConnection<LinePosition, LinePosition> jumping =
    new HorizontalLinePositionConnection();
Variable variable = new Constant(5);
List<String> names = List.of("Max", "Maria");
 
// with inferred types
var no = new No();
var more = new BigDecimalAmountIncrease();
var jumping = new HorizontalLinePositionConnection();
var variable = new Constant(5);
var names = List.of("Max", "Maria");

变量类型固然重要,但是变量名称才是决定性因素。类型描述了Java整个生态系统(JDK)、通用用例(库或框架),以及业务领域(应用程序)。 但一个又一个渺小的变量名称才是串起这一些的枢纽。就像JS那样,不也风靡世界吗?

使用var的时候,变量名可以忽略类型名近乎以顶格的方式存在,特别是当你双击选中其中一个变量名的时候,不同以往的满天星的呈现,如今可以排列地整整齐齐。Kotlin的这一点不也广受欢迎?

如上所述,舍弃掉变量类型显示声明,换来了另一种方式的可读性,我们要做的就是适应。

Finding A Style

当然,使用var固然容易,但是我们需要在可读性和简洁性之间取得平衡。甲骨文的Java语言架构师,负责Amber项目的Brian Goetz给了我们启示:

当我们需要使代码更清晰,更简洁的同时不会丢失掉一些重要信息,那就使用var。

为什么不用 val/let?

许多使用var为主变量的语言会为不可变变量提供一个额外的关键字,通常是val或者let,但是我们在Java 10将会使用final var,促成这个结果的原因有以下几点:

  • 不可变变量比局部变量更加重要。
  • 从Java 8开始,我们Effectively final的概念(局部内部类和匿名内部类访问的局部变量必须由final修饰,java8开始,可以不加final修饰符,由系统默认添加。java将这个功能称为:Effectively final 功能)
  • 引入var的赞扬度很高(74% 强烈支持, 12% 中度支持) 反观 var/ val 与 var/ let 的组合则含糊不清。

这个结果其实有些令人失望的,让val或者let替代final var不是也挺好的吗?

Well, maybe in the future… until then we have to use final var.

总结

在Java 10之后你在声明局部变量类型的时候可以使用var来告知编译器进行类型推断,取代之前的类名或接口名。这仅仅发生在变量初始化的阶段,就像 var s = "";这样。 此外,for循环中的索引变量类型也可以使用var。它由编译器推断类型,然后将推断出的类型写入字节码中,也就是说它对运行时并没有任何影响,仅仅是一个语法糖,Java仍然是一种静态语言。

除了局部变量之外,另外在属性和方法返回值类型中,不能使用var。 这样做是为了避免引起一些无法预知的错误,使用的时候尽量使需要推断的变量靠近它声明的地方,从而缓解可读性问题。

尽管引入var变量会使代码可读性变得更糟,但此次的新特性为开发者提供了一种在编写复杂表达式的时候寻求了一个新的契机。

略有改动 原文参考