写点什么

Java 10 var 关键字深度解读

  • 2018-04-01
  • 本文字数:4064 字

    阅读完需:约 13 分钟

北京时间 2018 年 3 月 21 日,Java 10 如约而至。虽然这一版本带来的特性并不是非常多,但其中有一项仍然成为大家关注的热点,它就是局部变量类型推断(JEP 286)。JEP 286 引入了 var,用于声明局部变量,例如:

var users = new ArrayList<User>();事情就是这么简单。不过,这篇文章将会讨论更多有关 var 的内容,比如什么时候可以用 var、什么时候不能用 var、var 对可读性的影响,以及为什么没有使用 val。

使用 var 代替类型声明

作为 Java 开发者,在声明一个变量时,我们总是习惯了敲打两次变量类型,第一次用于声明变量类型,第二次用于构造函数,比如:

URL codefx = new URL("http://codefx.org")我们也经常声明一种变量,它只会被使用一次,而且是用在下一行代码中,比如:

复制代码
URL codefx = new URL("http://codefx.org")
URLConnection connection = codefx.openConnection();
Reader reader = new BufferedReader(
  new InputStreamReader(connection.getInputStream()));

这样也不算太糟糕,就是有点啰嗦。尽管 IDE 可以帮我们自动完成这些代码,但当变量总是跳来跳去的时候,可读性还是会受到影响,因为变量类型的名称由各种不同长度的字符组成。而且,有时候开发人员会尽力避免声明中间变量,因为太多的类型声明只会分散注意力,不会带来额外的好处。

从 Java 10 开始,开发人员可以使用 var 让编译器自己去推断类型:

复制代码
var codefx = new URL("http://codefx.org");
var connection = codefx.openConnection();
var reader = new BufferedReader(
  new InputStreamReader(connection.getInputStream()));

在处理 var 时,编译器先是查看表达式右边部分,也就是所谓的构造器,并将它作为变量的类型,然后将该类型写入字节码当中。

这样可以少敲几个字,但更重要的是,它避免了信息冗余,而且对齐了变量名,更容易阅读。当然,这也需要付出一点代价:有些变量,比如例子当中的 connection,就无法立即知道它是什么类型的。虽说 IDE 可以辅助显示出这些变量的类型,但在其他场景下可能就不行了,比如在代码评审的时候。

另外,你不需要担心变量名或方法名会与 var 发生冲突,因为 var 实际上并不是一个关键字,而是一个类型名,只有在编译器需要知道类型的地方才需要用到它。除此之外,它就是一个普通合法的标识符。也就是说,除了不能用它作为类名,其他的都可以,但极少人会用它作为类名。

局部变量类型推断是一个非常直观的特性,不过你可能会想:

  • 这到底是 Java 还是 JavaScript?
  • 可以在哪些地方使用 var?
  • var 会影响可读性吗?
  • 为什么没有 val 或 let?

不,这不是 JavaScript

首先我要说明的是,var 并不会改变 Java 是一门静态类型语言的事实。编译器负责推断出类型,并把结果写入字节码文件,就好像是开发人员自己敲入类型一样。

下面是使用 IntelliJ(实际上是 Fernflower 的反编译器)反编译器反编译出的代码:

复制代码
URL codefx = new URL("http://codefx.org");
URLConnection connection = codefx.openConnection();
BufferedReader reader = new BufferedReader(
  new InputStreamReader(connection.getInputStream()));

从代码来看,就好像之前已经声明了这些类型一样。事实上,这一特性只发生在编译阶段,与运行时无关,所以对运行时的性能不会产生任何影响。所以请放心,这不是 JavaScript。

如果你仍然担心不显式声明类型会让代码变得更糟糕,那么我倒要问你了,你在使用 lambda 表达式的时候会声明参数的类型吗?

rhetoricalQuestion.answer(yes -> "see my point?");## 哪些地方可以使用 var(或哪些地方不能使用 var)

JEP 286 的标题“局部变量类型推断”就已经暗示了哪些地方可以使用 var:局部变量。更准确地说,是那些带有构造器的局部变量声明。但像这样的就不行了:

复制代码
// 不行
var foo;
foo = "Foo";

必须写成:

var foo = "Foo";除此之外,var 也不能用在“多元表达式”中,如 lambda 和方法引用:

复制代码
// 这些都不行
var ints = {0, 1, 2};
var appendSpace = a -> a + " ";
var compareString = String::compareTo

除了局部变量,for 循环是唯一可以使用 var 的地方:

复制代码
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 代码块仍然需要显式声明类型。

复制代码
// 这样也是不行的
private var getFoo() {
  return "foo";
}

避免“Action At A Distance”错误

将 var 限定在局部变量上并非技术方面的局限,而是设计上的决定。确实,如果能够像下面这样岂不更好?

复制代码
// 编译器推断出类型 List<User>
var users = new ArrayList<User>();
// 这样就不行了,会出现编译错误
users = new LinkedList<>();

按照预期,编译器应该能够找出最具体的那个类型,但实际上它不会。JDK 团队想要避免“Action At A Distance”错误(AAD),也就是说,他们希望在某处修改了代码不会影响到其他很“远”的地方。比如:

复制代码
// id 被推推为`int`
var id = 123;
if (id < 100) {
  // 此处省略了很长的代码
  // 调用了其他类的方法
} else {
  // 此处也省略了很长的代码
}

现在,我们加入一行:

id = "124"这样会发生什么?if 代码块会抛出一个错误,因为 id 变成了字符串类型,所有不能使用小于号进行比较操作。这个错误距离代码修改的地方很“远”,而其根源就是因为对一个变量重新赋值。

这么看来,将类型推断限定在带有构造器的局部变量声明上是有它的道理的。

为什么不推断字段和方法的类型?

字段和方法的作用域比局部变量大得多,所以更有可能出现 AAD 错误。在最糟糕的情况下,修改一个方法的参数类型可能导致二进制文件的不兼容和运行时错误。

因为非 private 的字段和方法是类契约的一部分,它们的类型不能通过推断来获得。不过,private 的字段和方法似乎可以使用类型推断,但问题是这样会让这个特性看起来非常奇怪。

局部变量属于实现细节,通常不会在很“远”的地方引用这些变量,所以就没有必要严格、显式和啰嗦地给它们声明类型了。

为什么要使用 var?

相比其他年轻的编程语言,Java 代码的啰嗦是开发人员最大的痛点之一,也是饱受 Java 开发人员诟病的一个地方。为此,Amber 项目开发了 var,旨在“开发出一些小的 Java 语言特性,以便提高效率”,其目标是降低 Java 代码编写和阅读的繁琐程度。

局部变量类型推断正好迎合了这一目标。在编写代码时,声明变量的方式更简单了,尽管这类代码有大半可以使用 IDE 生成,或者使用 IDE 的重构功能进行修改。

除了让变量声明变得更简单,修改起来也很容易。这话怎么说?有些变量的类型真的很难看,比如那些带有泛型的企业级类名:

InternationalCustomerOrderProcessor<AnonymousCustomer, SimpleOrder<Book>> orderProcessor = createInternationalOrderProcessor(customer, order);因为类型名称太长了,结果把变量名推到了代码的右边。如果限定了每行只能容纳 150 个字符,那么变量名还有可能被推到下一行显示。这些对于可读性来说都是一种伤害。

var orderProcessor = createInternationalOrderProcessor(customer, order);使用 var 就显得不那么累赘了,一眼就能看到头。

总之,使用 var 的意义不在于减少字符数量,而是为了不那么啰嗦和累赘。

对可读性的影响

现在让我们来讲讲可读性。确实,类型的缺失会让事情变得更糟糕,不是吗?一般来说,确实是的。在阅读代码时,类型是很重要的一个因素。尽管 IDE 可以帮助显示出推断的类型,但如果这些类型直接显示在代码中看起来不是更方便吗?

这是 var 在可读性方面的一个不足,不过,它却带来了另一个优势,那就是变量名对齐:

复制代码
// 显式类型
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");
 
// 推断类型
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 的类)、一般场景(类库或框架)或业务领域(应用程序)的一般性概念,所以类型一般会有通用的名字。而变量名处在很小的上下文中,它们的名字应该要更精确一些。

这种可读性方面的改进可能会导致出现更多带有构造器的局部变量声明,因为这样在编写代码和阅读代码时会更加方便。

为什么没有使用 val/const/let?

其他很多使用了 var 的编程语言也会为不可变变量提供一个额外的关键字,比如 val、const 或 let。但 Java 10 没有使用这些关键字,所以我们必须使用 final var 来声明不可变变量。究其原因,可能是因为:

  1. 虽说不变性很重要,但对于局部变量来说,这种重要性程度没有那么高。
  2. Java 8 引入了“隐式”final 的概念,所以在我们看来,局部变量就已经是不可变的。
  3. 大部分人同意使用 var(74% 的人强烈同意,12% 的人基本同意),而对 var/val 和 var/let 的反馈则显得有点含糊不清。

我同意前面两点,至于第三点只能勉强接受,但对结果还是感到有点失望。或许等到了有一天,我们不得不使用 final var 的时候,是不是可以考虑使用 val 或 let?

总结

在声明局部变量时,可以使用 var 代替具体的类名或接口名,让编译器自己去推断变量的类型。当然,只有在声明并且立即初始化变量的情况下才能使用 var。for 循环中的下标也可以使用 var 来声明。编译器会把推断出来的类型写入字节码,不影响运行时。Java 仍然是一门静态类型的语言。

除了局部变量,var 不能被用于字段或方法上,这样做是为了避免 AAD 错误。

虽说 var 有可能让代码变得更糟,但作为 Java 开发者,应该尝试在变量声明和嵌套表达式或链式表达式之间做出权衡,写出可读性更高的代码。

英文原文: https://blog.codefx.org/java/java-10-var-type-inference/

感谢郭蕾对本文的审校。

2018-04-01 18:2910991
用户头像

发布了 731 篇内容, 共 416.8 次阅读, 收获喜欢 1979 次。

关注

评论 1 条评论

发布
用户头像
太多类名字起的太糟了。 var 提高了阅读代码效率
2020-03-19 15:54
回复
没有更多了
发现更多内容

基于 Caddy 部署盘古 Admin 实现流量网关

码农大熊

盘古开发框架

只要是做乙方,欧美白人下班后也别想失联?

SAP虾客

IT行业 乙方 工作与生活分开 欧美 TICKET

如何用 7 分钟玩转函数计算?

阿里巴巴云原生

阿里云 Serverless 云原生

大数据开发技术培训班怎么选

小谷哥

前端程序员培训学习需要学多久?

小谷哥

OSCS开源安全周报第22期:NuGet 仓库中被发现 13.5 万个包含钓鱼地址的组件包

墨菲安全

GitHub标星已达26K+,鹅厂技术总监手写分布式架构体系笔记

小小怪下士

Java 程序员 分布式

一文读懂于Zebec生态中的潜在收益方式

鳄鱼视界

【电路设计】避开元器件的这些“坑”,多年的心梗都治好了!

攻城狮华哥

工具 PCB PCB设计

MegEngine Windows Python wheel 包减肥之路

MegEngineBot

深度学习 开源 MegEngine

YonBuilder移动开发平台 AVM框架 封装省市区级联选择弹框

YonBuilder低代码开发平台

开发者 AVM

前端编程学习有没有必要参加?

小谷哥

火山引擎DataTester科普:A/B实验常见名词解释

字节跳动数据平台

大数据 AB testing实战 12 月 PK 榜

部分双机热备软件详细介绍-行云管家

行云管家

高可用 双机热备 双机

HIFIVE音加加提供曲库、评分、修音功能的K歌SDK-Android版本

数到3变暖男i

API 社交泛娱乐 娱乐社交 K歌 K歌SDK

一文带你了解EiPaaS和EiPaaS的国际趋势

华为云开发者联盟

云计算 后端 华为云 12 月 PK 榜

阿里二面被问MySQL的事务隔离级别,结果回去等通知了

程序员小毕

MySQL 数据库 程序员 面试 后端

如何在小程序中完成支付进件

Towify

微信小程序 编辑器 无代码

学习大数据培训和自学哪个比较好

小谷哥

行业认可|墨菲安全登信息通信软件供应链安全社区优秀榜单

墨菲安全

参加java培训对学习程序员有用吗?

小谷哥

西藏等保测评公司有哪些?共有几家?

行云管家

等保测评 等保测评公司 西藏

TDengine与中泰证券正式签约,打造金融量化交易场景解决方案

TDengine

数据库 tdengine 时序数据库

“2022混合云TOP50”重磅发布 天翼云问鼎榜首

Geek_2d6073

如何通过 NFTScan 发掘 NFT 项目的内在价值

NFT Research

区块链 NFT

皮皮App发起爱心捐赠,让城市里的荧荧之光,给乡村孩子带去一片暖冬

联营汇聚

Java 10 var关键字深度解读_语言 & 开发_nipa_InfoQ精选文章