新时代背景下的 Java 语法特性
前言
从Java5作为我职业生涯的开篇开始算起,不知不觉,已经走过了近10个年头。打开shell,敲一把java -version
,我相信,Java6-8的用户群体仍然是当下主流。或许,我们每个人都拥有诸多令人无法轻易反驳的理由而选择停留在当下,但仔细想想看,Milestone版本的Java8所带来的Lambda特性截至目前已是6年前的产物了。
早期Java的大版本更新速率一直保持着龟速前行,且经常跳票,以至于落人诟病,导致很多连业务代码都写不明白的人,愿意整天花着大把时间在社区吐槽Java太重,不如A、B、C语言轻量;样板代码冗繁,影响开发效率。如今,Java做到了快速响应开发者们的反馈,却又被部分人认为Java的更新频率太快,根本没时间学习,对于这部分人,我只说一句:“Shut up,troll”。仔细想想看,某些新语言的诞生实质上是为了更好的迎合当下(比如:解决特定领域的计算问题、改进某些语法痛点,以及带来开发实惠等);我曾经说过,“Java真正强大的地方是因为拥有全世界最多的技术拥护者和开源社区支持,他们无时无刻都保持着最充沛的体力与思维,一步一步地驱动着Java技术的走向”;因此,Java走到今天,靠的就是自身一次次华丽蜕变来抵御外界的流言蜚语、谩骂,甚至是看衰。
本篇博文,我会从Java9开始,过渡到目前GA版本的Java14,从中挑选出一些最具代表性的新增语法特性和优化改进项来为大家进行讲解。相信仔细阅读后,除了能够让你有所裨益外,还能让你重新审视Java,Java书写的代码居然如此精美和高效。接下来,请打开你的IDE,和我一起使用New Java书写新时代的篇章。
进一步强化的集合API
早期,如果我们期望在程序中定义一个带有少量元素,且不可修改的集合或Map实例时,我们往往需要依赖Collections类来进行实现。示例1-1:
示例1-1展示了定义一个不可变集合或Map实例的基本用法。首先,我们需要提前定义好Set实例,然后执行添加操作,最后再将其转换为不可变实例,相信大部分同学的直观感受都是觉得这样的做法过于冗长,在实际的开发过程中,我们将不得不花费大量的时间去建设样板代码。值得庆幸的是,Java9之后我们仅需一行有效代码即可完成同样的事情。示例1-2:
在此大家需要注意,接口的静态方法不允许被继承和重写,因此List、Set和Map的static of()方法在其派生类中无法使用;其次of()方法为重载方法,在List和Set中,提供有固定参数和可变参数等2种入参形式,当size <= 10时,缺省使用固定参数来避免因可变参数引起的数组资源消耗和GC损耗;而Map.of()
方法并没有提供可变参数的入参形式,但提供有等价的Map.ofEntries()
方法,只是使用稍嫌麻烦。示例1-3:
Java9为List、Set和Map接口新增了static of()
方法,而Java10则对其进行了进一步的强化,增加了static copyOf
()方法。实际上copyOf()
方法的作用类似于Collections.unmodifiableSet\List\Map()
,目的就是为了能够将集合和Map实例转换为不可修改的实例。示例1-4:
在此大家需要注意,如果集合和Map实例本身就是不可变时,调用copyOf()方法将会直接返回源对象,而不会返回一个新的AbstractImmutableList对象实例。源码示例1-5:
接口私有方法支持
早在Java8时代,Java设计者们就允许开发人员在接口中使用关键字default和static定义方法实现,这着实让我们惊艳了一把。在实际的开发过程中,我认为在接口中定义default方法最大的好处就是完全可以替代接口适配器的任务,不必再单独引入一个适配器类来进行空重写,派生类按需重写即可,极大程度上减少了工程中类文件的维护成本,以及很好的屏蔽了接口向后变动时所带来的影响和变化。
在某些时候,我们往往会选择将一些通用的静态方法整体封装在一个工具类中,但如果目标方法有充分的语义原因和某个概念相关,那么这个方法就不再应该被放置在某个工具类中,而是应该包含在具体的类或接口中,以便于后续维护。因此,针对这样的方法,Java8之后我们就可以直接在接口中通过使用static关键字来进行定义。
而在过渡到Java9之后,接口的功能特性被进一步强化。我们都知道,被default和static关键字修饰的方法实质上是透明的,是允许被外界访问的,但如果我们希望接口中所定义的方法不被公开化,那么私有方法的介入就是一个很好的特性补充。示例1-6:
进一步强化的try-with-resources语句
早在Java7时代,程序中只要是直接或间接实现了java.lang.AutoCloseable
接口的派生实现,都可以享受自动资源释放的所带来的便利,开发人员无需在程序中使用try-finally语句的方式手动释放一系列的可回收资源(比如I/O资源、DB连接资源、TPC/UDP连接资源等),从而可以很好的避免因遗忘等原因所带来的资源浪费或耗尽等情况发生。
但try-with-resources语句在使用上仍然还是存在一定的样板化。资源的声明一定强制要求在try()语句中完成,而Java9的到来则很好的规避了这个问题,一定程度上弱化了束缚,使得开发人员能够用更加随性的方式来享受自动资源管理所带来便利,也就是说,可回收资源的声明不再强制要求一定要在try()语句中完成。示例1-7:
匿名内部类的泛型自动类型推导
早在Java5的时候,Java的设计者们就为开发人员提供了泛型特性用于规避类型向下转换时可能出现的类型转换错误。泛型的本质实质上就是参数化类型,简单来说,就是通过指定参数的形式来操作数据类型。Java7新增了泛型的自动类型推导特性,使得开发人员在定义泛型类型时,不必在变量的初始和赋值2端都指定泛型类型,仅需在声明一侧指定即可,编译器会在编译时会根据其上下文信息来自动推导出正确的参数类型。关于泛型的自动类型推导一直就是一个饱受争议的话题,很多人认为省略泛型类型有利于增加代码的可读性,而部分人又认为需要类型信息的介入才能够更好的理解代码,当然,实际上我更倾向于后者。
Java8阶段针对泛型的自动类型推断其实已经做过一次强化,它允许开发人员省去Lambda表达式中所有的参数类型,编译器会在编译时根据其上下文信息实现参数类型的自动推导。过渡到Java9后,匿名内部类在泛型特性的使用上同样也做出了强化,开发人员仅需在变量的声明端指定泛型类型即可。示例1-8所示:
进一步强化的Stream API
Java8的到来几乎涉及到整个Java核心类库的改变,其中Lambda表达式和Stream API是其中最为重要的2项改进。Lambda表达式是一种对行为的抽象,能够有效帮助开发人员写出更具维护性、扩展性,以及可读性的代码,极大程度上减少样板代码的生成。而Stream API则使得开发人员能够站在更高的抽象层次上操作对象和集合框架,以及能够使用非常简单的方式来实现并行计算。
Stream API早期,我们经常会使用到filter()方法来过滤掉那些不满足匹配条件的项目。但存在一种情况,如果流中出现不满足匹配条件的元素时,我们期望的是终止流,filter()方法显然无法满足。值得庆幸的是,Java9的到来进一步增强了Stream API,不仅提供了takeWhile()
方法来填补这块空白,还引入了dropWhile()
、ofNullable()
,以及iterate()
等方法来帮助开发人员写出更好的代码。接下来,我会先从takeWhile()方法开始讲起。示例1-9:
示例1-9中,takeWhile()
方法同filter()
一样,接收一个函数作为入参,返回值为boolean类型。当流中出现不满足匹配条件的项目时,立即终止流,不会像filter()那样一直迭代到末尾元素。理解takeWhile()
方法的作用后,再来理解dropWhile()
方法就会变得非常容易,因为后者是前者的相反操作。
iterate()函数在示例1-9中就已经为大家进行了演示,它是一个迭代函数,作用类似于for(int i = 0;i<10;i++)
代码块,其中第1个入参是一个初始种子值,第2和第3个入参类型都是函数,hasNext用于检查序列中是否还有元素,而next则用于获取序列中的下一个元素。
ofNullable()
方法是Stream.of()
方法的补充,非空检测并创建一个单元素的Stream。示例1-10:
这里顺带补充一下,Java10针对Stream API也增加了定义不可变集合和Map实例的Collectors收集器方式。示例1-11:
var关键字支持
Java毋庸置疑是一门强类型的语言,那么这就使得任何一个变量在使用前都必须先明确其类型,这是长久以来Java延续至今的语法特性。但随着Java10的正式来临,我们终于可以使用var关键字来定义局部变量了,编译器会在编译时会根据其上下文信息来自动推导出正确的变量类型,从而再也无需显式声明变量类型,极大程度上减少了样板代码的编写。示例1-12:
在此大家需要注意,如果项目中大量使用var关键字定义局部变量必然会降低代码的可读性,尤其是那些使用变量接收方法返回值的代码片段,由于“年久失修”又或者是因团队中其他人编写的原因,迫使我们不得不花费多余的时间去确认方法签名。其次,var关键字仅限于定义局部变量,不支持:方法入参、全局变量、方法出参等。
针对Lambda表达式中形式参数声明的语法,Java语法层面定义了2种声明形式,分别是显式和隐式。除了在那些需要强制声明类型的场景下,绝大多数情况下,我们都在使用后者。随着Java11的正式来临,Lambda表达式中也允许使用var关键字来显示声明类型。示例1-13:
现代化的HTTP/2 Client支持
早在Java9时代,JEP-110提案就曾计划向JDK中引入标准化的HTTP/2 Client来替代Java1.x时代古老的HttpURLConnection API和降低外围构件依赖,直至Java11正式来临后,该提案才正式从孵化项转正。也就是说,如今,当我们需要在程序中编写HTTP客户端代码时,武器库中除了耳熟能详的Apache HttpClient、OkHttp等第三方构件外,缺省支持Reactive-Stream的JDK HttpClient也不失为一个正确的选择,同时这也是语法层面Java11带给开发者们最大的惊喜。示例1-14:
示例1-14中我简单演示了JDK HttpClient的基本用法,相对于直接使用HttpURLConnection API而言,代码的可读性和易用性提升了不止一个数量级。HttpClient.newHttpClient()
会返回一个HttpClient
对象,由该对象负责定义一些通用的、基础的Http连接信息(比如:Http协议、Http代理、重定向方式、连接超时时间、身份认证、SSL证书等),当然我们也可以直接调用HttpClient.newBuilder().build()
来指定具体的连接参数信息。示例1-15:
HttpRequest
和HttpResponse
分别代表着Http的请求对象和响应对象,二者由HttpClient
串联起来即可完成一次完整的Http请求/响应过程。
进一步强化的String类型
String的改变主要包含2点,首先是String的底层实现在Java9之后正式从16bit的char[]类型改为了数据结构更加紧凑的byte[]类型,以此来达到提升String类型空间效率的目的。其次,Java13之后开始正式支持多行字符串的文本块语法定义(目前是预览功能,需要显示开启,正式功能会在Java15中开放)。在实际的开发过程中,业务代码中必然会充斥着大量的多行字符串,比如:SQL、JSON、HTML代码片段,甚至是banner信息等,为了让其具备更好的可读性和维护性,我们往往会使用换行符"\n"将其进行分割,并通过符号“+”进行串联。示例1-16:
如今,使用String为我们提供的“二维文本块”方式即可替代之前面临的诸多不便。示例1-17:
示例1-17为大家演示了如何在程序中定义“二维文本块”,我们熟悉的单行字符串内容是被双引号“”包裹,而文本块则是被3个双引号的定界符(开始定界符和结束定界符)包裹。并且文本块中,每一行都会实现自动换行,原则上无需大家再手动加上换行符“\n”,当然如果我们希望文本块的最后一行不实现自动换行,则可以将内容与结束定界符并行在同一行中。
除了可以使用定界符来定义文本块外,语法层面上我们通过符号“+”也可快速实现将单行字符串和文本块串联合并。示例1-18:
最后,针对文本块中的转义操作,我简要说明一下。文本块中是允许使用转义符“\”的,但绝大多数情况下,我们都无需使用。比如:那些包含在单行字符串中的双引号我们是需要通过转义符“\”进行显式转义的,但在文本块中却并不需要;其次由于定义在定界符中的多行字符串会实现自动换行,如果无需换行,可通过转义符“\”进行声明。示例1-19:
但是有一种情况,我们是需要强制使用转义符的,那就是文本块中还包含有另外的文本块。示例1-20:
进一步强化的switch语句
Java7时代,Java设计者们针对switch语句的表达式值做了一次特性补充,除缺省支持的byte、short、int、char,以及enum等5种数据类型外,还额外引入了String类型。但这似乎并不能从根本上改变什么,switch语句依旧保持着冗繁和极其容易出错的语法风格,试想一下,如果我们在case之后忘记了break,那么所带来的影响将有可能会是灾难性的。因此,在绝大多数情况下,多路分支语句的技术选型我更倾向于传统的if-else-if形式。
Java设计者们似乎也意识到了这个问题,因此从Java12开始,针对switch语句的改进就被写入了JEP提案,直至Java14正式来临后,switch语句终于迎来了新生。强化后的switch语句引入了一种新的标签形式,即:case L ->
,这看起来类似于Lambda表达式的语法风格。示例1-21:
示例1-21演示了new switch语句的基本用法。基于case L ->
标签后,我们无需再显式定义break语句,也就是说,如果匹配标签,程序仅会执行标签右侧的代码逻辑,这大幅度提升了switch语句的安全性;其次,如果多个case标签都需要执行相同的结果,我们通常的做法是定义多个case标签,但如今我们只需使用符号“,”来分割条件值,以此来简化原本冗繁的写法。
当然,switch的改变不止于此。在实际的开发过程中,我们除了可以将switch用作多路分支语句外,还可以作为表达式使用。示例1-22:
在此大家需要注意,case L ->
标签右侧的代码仅支持表达式、代码块,以及throw语句。如果需要定义代码块,那么表达式的返回值则需要引入关键字yield来进行返回。示例1-23:
null-detail支持
空指针异常相信大家都不会感觉到陌生,虽然Java设计者们已经为开发人员提供了各式各样的手段来检测和避免NullPointerExceptions异常,但在实际的开发过程中,这样的现象却仍然无法完全杜绝。那么当程序中出现此异常时,我们从异常堆栈信息中唯一能够获取到的有效信息仅仅只是具体的行数,而究竟是具体哪一个参数引起的却浑然不知,这对于定位问题非常不友好,要知道线上每耽误的一秒钟,都是在降低系统的全年可用率。值得庆幸的是,Java14为开发者们提供了null-detail支持,我们仅需通过命令行选项显式启动即可。示例1-24:
假设程序中出现空指针异常时,堆栈信息中将会明确具体原因。示例1-25:
截止本篇博文成稿之日起,我们离GA版本的Java15正式上线还有半个月左右的时间,那么请认真问问你自己,准备好迎接它的到来了吗?
码字不易,欢迎转发
版权声明: 本文为 InfoQ 作者【高翔龙】的原创文章。
原文链接:【http://xie.infoq.cn/article/655943e5f85e6f79ffbd03047】。文章转载请联系作者。
评论 (3 条评论)