设为首页 加入收藏

TOP

Java 10 var关键字深度解读(一)
2018-04-08 08:51:16 】 浏览:245
Tags:Java var 关键字 深度 解读

北京时间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 script


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


下面是使用IntelliJ(实际上是Fernflower的反编译器)反编译器反编译出的代码:
URL codefx = new URL("http://codefx.org");
URLConnection connection = codefx.openConnection();
BufferedReader reader = new BufferedReader(
  new InputStreamReader(connection.getInputStream()));


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


如果你仍然担心不显式声明类型会让代码变得更糟糕,那么我倒要问你了,你在使用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错误。在最糟糕的情况下,修改一个方法的参数类型可能导致二进制文件的不兼容和运行时错误。


因为非p

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇C++ 编译器的函数名修饰规则 下一篇JavaScript中的数组对象

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目