通过本文了解Java8以来这门语言的发展。
作者 | Dávid Csákvári
译者 | 弯月,责编 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下为译文:
当年Java 8引进的Stream和Lambda是一项重大改进,编写函数式编程风格的代码不再需要写大量的样板代码。虽然近期的版本并没有引入如此重大的改进,但Java还是引入了很多小改进。
这篇文章总结了Java 8之后引入的语言改进。如果你想了解新平台背后的JEP,请参考这篇文章:
https://advancedweb.hu/2019/02/19/post_java_8/
局部变量类型推断
var关键字可能是Java 8之后最重要的语言改进了。该关键字最初在Java 10引入,在Java 11得到了大幅改进。
有了它,我们就可以在定义局部变量时省略类型定义,减少繁文缛节:
var greetingMessage = "Hello!";
尽管看上去这很像JavaScript的var关键字,但它并不是动态类型。
引用如下JEP的一段话:
我们希望通过减少编写Java代码时的繁文缛节来改善编程的体验,同时维持Java的静态类型安全。
这样定义的变量的类型会在编译时进行推断,上述示例中推断的类型为String。使用var而不是显式指定类型,可以让代码更加简洁,故而可以提高代码的可读性。
下面是类型推断的另一个例子:
MyAwesomeClass awesome = new MyAwesomeClass();
显然,知许多情况下这个特性都可以改进代码质量。但是,有时候还是使用显式类型定义更好。我们来看看一些不宜使用var替换类型定义的情况。
随时考虑可读性
第一种情况就是从源代码中删除类型定义可能会降低可读性的情况。
当然这种情况还可以借助IDE,但在代码审核过程中,或者需要快速阅读代码的情况下,这样做就可能影响可读性。比如工厂模式,你只能去寻找负责生成对象的代码来确定生成的对象类型。
下面是一个小测验。下面的代码使用了Java 8的日期和时间API。猜一猜下面代码中的变量类型:
var date = LocalDate.parse("2019-08-13"); var dayOfWeek = date.getDayOfWeek(); var dayOfMonth = date.getDayOfMonth();
做完了?答案如下所示。
第一行很直观,parse方法返回LocalDate对象。但是后两个你必须对API有一定了解才能得出正确答案:dayOfWeek返回java.time.DayOfWeek,而dayOfMonth返回int。
使用var的另一个潜在问题是,阅读者不得不进一步依赖注释。考虑下面的代码:
private void horriblyLongMethod() { // ... // ... // ... var dayOfWeek = date.getDayOfWeek(); // ... // ... // ... }
有了上一个例子的经验,我打赌你肯定会猜它是java.time.DayOfWeek。但这次是个整型,因为本例中的date是Joda时间。这是个不同的API,行为也略有不同,但你没有发现,因为这个方法非常长,而你并没有阅读所有代码。
如果这里给出了显式类型定义,那么确定dayOfWeek就非常容易。而使用var时,阅读者首先要找到date变量的类型,并检查其getDayOfWeek的行为。在IDE中很容易理解,但快速阅读代码时就没那么容易了。
注意保留重要的类型信息
第二种情况是,使用var会丧失所有类型信息,甚至导致无法推断。大多数情况下这个问题会被Java编译器捕获。例如,var不能推断lambda或方法引用,因为在这些特性中,编译器依赖左侧的表达式来确定类型。
但是有一些例外。例如,var不能很好地用于菱形操作符。在创建泛型的实例时,菱形操作符可以让表达式右侧不那么繁琐:
Map<String, String> myMap = new HashMap<String, String>(); // Pre Java 7 Map<String, String> myMap = new HashMap<>(); // Using Diamond operator
由于该运算符只处理泛型类型,所以我们依然可以去掉一些冗余。我们可以通过var进一步简化:
var myMap = new HashMap<>();
这个例子是合法的,而且Java 11编译器甚至都不会发出警告。但是,我们没有为泛型类型指定任何类型,导致所有类型都必须推断,所以最后的类型是Map<Object, Object>。
当然,只需去掉菱形运算符就可以解决这个问题:
var myMap = new HashMap<String, String>();
另一个问题是在基本数据类型上使用var:
byte b = 1; short s = 1; int i = 1; long l = 1; float f = 1; double d = 1;
如果不给出显式类型定义,那么所有变量都会被推断为int。所以,使用基本数据类型时要使用类型字面量(例如1L),或者不要使用var。
务必阅读官方的风格指南
何时使用类型推断、怎样做不会破坏易读性和正确性,这些问题最终都需要你自己判断。经验法则是:遵循优秀的编程实践,比如良好的命名规则、尽力减小局部变量作用域等都会有很大帮助。请务必阅读官方有关var的风格指南(https://openjdk.java.net/projects/amber/LVTIstyle.html)和FAQ(https://openjdk.java.net/projects/amber/LVTIFAQ.html)。
虽然var有如此多的陷阱,但很幸运它的引入相当保守,现在只能用于作用域有限的局部变量。
而且,var的引入也十分谨慎,var并不是新的关键字,而是保留类型名。这就意味着,只有当作为类型名使用时才有特殊含义。任何其他位置出现的var依然只是个合法的标识符。
目前,var没有相应的不可修改版本(如val或const)来定义常量并推断类型。希望以后的版本能够添加这个关键字,在那之前我们可以先使用final var。
参考资料:
与Java 10的var的第一次亲密接触(https://blog.codefx.org/java/java-10-var-type-inference/)
Java局部变量类型推断详解(https://dzone.com/articles/var-work-in-progress)
Java 10:局部变量推断(https://www.journaldev.com/19871/java-10-local-variable-type-inference)
Project Coin带来的多项改进
Project Coin(JSR 334,https://jcp.org/en/jsr/detail?id=334)是JDK 7的一部分,它带来了许多方便的语言改进:
菱形运算符
try-with-resources语句
多catch和更精确的重新throw
在switch语句中使用字符串
二进制整形字面量和数值字面量中的下划线
简化的varargs方法调用
Java 9继续做出了许多小改进。
接口支持私有方法
从Java 8起可以给接口添加默认方法。在Java 9中,这些默认方法甚至可以调用私有方法,这样无需公开就可以复用代码。
尽管算不上重大改进,但能够让默认方法中的代码更简洁。
匿名内层类的菱形操作符
Java 7引入了菱形操作符(<>),让编译器推断构造函数的参数类型,来减少繁琐:
List<Integer> numbers = new ArrayList<>();
但是,以前该功能不能用于匿名内层类上。根据项目的邮件列表中的讨论(http://mail.openjdk.java.net/pipermail/coin-dev/2011-June/003283.html)可知,该功能没有作为菱形运算符的最初特性实现的原因是它需要JVM做出重大变更。
在Java 9中这个边缘情况终于解决了,因此现在的菱形运算符更通用:
List<Integer> numbers = new ArrayList<>() { // ... }
try-with-resources语句中允许使用没有发生实质性改变的变量
Java 7引入的另一项改进就是try-with-resources语句,从此程序员无需再担心释放资源的问题。
我们来演示一下这个功能。首先,在Java 7之前如果想正确关闭资源,需要这样写:
BufferedReader br = new BufferedReader(...); try { return br.readLine(); } finally { if (br != null) { br.close(); } }
有了try-with-resources语句,资源就可以自动释放,省却了许多繁文缛节:
try (BufferedReader br = new BufferedReader(...)) { return br.readLine(); }
尽管这个功能非常强大,但它有几个缺点(Java 9解决了这些缺点)。
虽然这种方法能处理多个资源,但很容易让代码丧失可读性。像这样在try关键字之后以列表的方式定义变量,看起来非常不符合常见的Java编程习惯:
try (BufferedReader br1 = new BufferedReader(...); BufferedReader br2 = new BufferedReader(...)) { System.out.println(br1.readLine() + br2.readLine()); }
而且,在Java 7之前,如果你想用这种写法来处理已有的变量,就必须定义一个临时变量。(例如JDK-8068948中的例子:https://bugs.openjdk.java.net/browse/JDK-8068948。)
为了解决这些问题,Java增强了try-with-resources,现在不仅能够处理新创建的变量,还能够处理局部常量,或者实际上不可变的局部变量:
BufferedReader br1 = new BufferedReader(...); BufferedReader br2 = new BufferedReader(...); try (br1; br2) { System.out.println(br1.readLine() + br2.readLine()); }
在这个例子中,变量初始化不需要跟try-with-resources的初始化部分写在一起。
不过需要注意的一个陷阱是,现在允许访问已经被try-with-resources释放的资源,绝大部分情况下这种访问都会失败:
BufferedReader br = new BufferedReader(...); try (br) { System.out.println(br.readLine()); } br.readLine(); // Boom!
下划线不再是有效的标识符
在Java 8中,如果使用下划线作为标识符,编译器就会发出警告。Java 9更进一步,禁止仅使用下划线作为标识符,将其留给未来的特殊语义使用。
int _ = 10; // Compile error
改进的警告
最后,我们提一下新版Java中有关编译器警告的改进。
现在可以用@SafeVarargs给私有方法添加注释,来避免错误的Type safety: Potential heap pollution via varargs parameter警告。(实际上,这个改动是之前提到过的JEP 213: Milling Project Coin中的一部分)。
有关Varargs的更详细内容可以看这里(https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html)。组合使用官方文档中提到的这些功能可能会造成泛型(https://docs.oracle.com/javase/8/docs/technotes/guides/language/generics.html)及其潜在问题(https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html)。
此外,从Java 9开始,编译器不会再因为导入被弃用的类型的import语句而产生警告。这些警告没有提供有用的信息,而且完全是多余的,因为在实际使用被弃用的类型成员时必然会产生警告。
文本讨论了Java 8之后的版本中这门语言本身的改进。随时关注Java平台很重要,因为现在的发布节奏很快,每六个月就会发布一个新版本,平台和语言也会发生相应的变化。
原文:
https://advancedweb.hu/2019/08/08/post_java_8_language_features
作者:Dávid Csákvári,全栈工程师,在Java和Web技术方面有十多年的经验。
【END】
技术人在关注TA! 戳↓↓↓
热 文推 荐
☞ 重 磅!全球首个可视化联邦学习产品与联邦pipeline生产服务上线
☞ 语音识别技术简史
☞ 物联网成网络安全防护新重点!
☞ 10 步教你接手同事的代码!
☞亚 马逊首席科学家李沐国内首次亲授「深度学习实训营」
☞ CSDN & 火星财经, 联手发起Libra超级节点竞选!
☞ "学了阿里中台,却依然做不好系统?" 聊聊阿里的项目管理
☞ 如何写出让同事无法维护的代码?
你点的每个“在看”,我都认真当成了喜欢