Java 的新未来:逐渐“Kotlin 化”

hanqiusy 2020-08-26

来自不同编程语言的竞争正促使 Java 不断吸收新特性,变得更能适应时代和开发人员的新需求。

我们行业始终有一个热门话题,那就是对 Java 的批判,这种批判很大程度上来源于 Java 的繁琐性以及它所生成的大量样板式代码,而这些代码在很多场景下是根本不需要的。

虽然我一直以来都很喜欢 Java,但是我不能说这些指责是错误的。的确,Java 的繁琐性以及数量不菲的杂乱代码在很多情况下会很烦人。在大多数情况下,我们不得不接受我们生活在一个不完美的世界这一现实,很多时候,我们必须两害相权取其轻。我们都知道,Java 并不完美,但是主要的问题在于在此之前为什么没有采取任何措施来解决这些问题呢?

我认为,变更需要耗费这么长时间的唯一原因就是 Java 缺乏足够的竞争。Java 语言之所以能够主导市场,可能就是因为缺乏真正的竞争对手,当然也要归功于 Sun 和 Oracle 先后做出的巨大努力。

Java 提供的强类型安全性,以及作为一个结构良好的语言所带来的其他特性,使其成为大型项目中非常流行的语言。通过使用 Java,我们很少会让事情变得难以收拾。Java 的另外一个重要特点是作为多平台语言,它运行在自己的虚拟机上,这使其能够完美契合很多的组织。如果你通过著名的 JIT 编译器启用了其自动性能优化的固有能力,那么在很多情况下都能将糟糕代码所带来的影响最小化,这样我们就有了一组使用 Java 的坚实理由。

但是,后来发生了什么呢?接下来的事情就是,能够像 Java 那样运行在相同 JVM 中的新语言推向了市场,这些语言消除了 Java 中最令人头疼的问题,并为开发人员提供了更好的环境,而且在很多情况下,它们的学习曲线非常平坦。

在继续下面的内容之前,我们简要回顾一下 JVM 语言的历史。

1. JVM 语言的历史

在开始之前,我想澄清一点,那就是我省略掉了一些现有的 JVM 语言,这主要是因为它们从来没有具备足够强的吸引力,不能视为我们行业中广泛使用的候选语言。那么,我们就开始快速回顾一下 JVM 语言的历史。

图片出自 Unsplash 站点,作者为 Maximilian Weisbecker

Java 的新未来:逐渐“Kotlin 化”

我们的旅程当然要从 Java 开始,它是 JVM 领域最古老和最流行的语言。

Java 语言最初是在 1996 年 1 月份正式发布的,所以它已经有 24 年的历史了,怎么样,还不错吧?最初,Java 是一种命令式语言,遵循纯粹的面向对象程序风格。它同时也是强类型语言,Java 的语法在某种程度上与 C++ 和 C 语言很相似,但它被认为是一个改进版本,因为使用 Java 编写代码要比使用 C 或 C++ 容易得多。另外,在它的批评者中,最大的争议在于其繁琐性。

第二个发布的 JVM 语言是 Groovy,它最早出现于 2003 年,不过其第一个官方和标准版本 1.0 是 2007 年才发布的。Groovy 的好处在于,它还可以用作脚本语言。Groovy 是一种动态类型语言,所以类型检查是在运行时进行的;这也是一些开发人员不喜欢 Groovy 的原因之一。我们使用 Groovy 编写代码,在编译时看起来它是正确的,但是在运行时,我们才会发现其中有问题。

Java 的新未来:逐渐“Kotlin 化”

图片来源:维基百科

接下来出现了另外一种流行的语言,也就是我们要讨论的 Scala。Scala 在 2004 年正式发布,它为 JVM 领域带来了一种新的编程模型,也就是函数式编程和它的声明式方法。我们可以确定,Scala 率先引入了不可变性(immutability)的理念,所以它对 Java 的转变做出了重要贡献。但另一方面,Scala 的批评者并不喜欢它,因为它语法复杂,可读性一般比较差。

Java 的新未来:逐渐“Kotlin 化”

图片来源:维基百科

JVM 领域出现的下一个语言是 Clojure,它是一个纯函数式语言,最近变得非常流行,但是它最早出现于 2007 年。Clojure 是一种基于 LISP 的语言,其特点是简单和使用纯函数。在它的缺点中,值得一提的是动态类型(与 Groovy 相同),而且学习曲线更陡峭,因为它的语法与其他 JVM 语言完全不同。如果你对学习 Clojure 感兴趣的话,那么应该阅读如下这两本好书:“The joy of Clojure”和“Programming Clojure(第三版)”。

Java 的新未来:逐渐“Kotlin 化”

图片来源:维基百科

最后但同样重要的是 Kotlin!Kotlin 在 2016 年 2 月份首次发布,从那时开始,它就变得不断流行起来。该语言是由 JetBrains 公司设计的,它有一个明确的目标:消除 Java 中所有常见的问题。它的设计方式保留了 Java 所有的优点,但是消除了 Java 的大多数问题,这也是它为何如此流行的原因,很多人甚至认为在未来的几年中它有可能会击败 Java。如果你想要了解 Kotlin 的更多内容的话(我鼓励你这样做,因为它是一门伟大的语言),那么我推荐你阅读“Kotlin in Action”一书,对于 Java 开发人员来说,这是一本入门 Kotlin 的优秀图书。

Java 的新未来:逐渐“Kotlin 化”

图片出自 Unsplash 站点,作者为 Louis Tsai

这就是最重要的 JVM 语言,我们省略了一些不太流行的 JVM 语言,但是我们可以提一下它们的名字:Jython、JRuby、Ceylon、Fantom 等等。你可以通过该地址获取现有 JVM 语言的全部列表。

我们可以发现,在最初的八年或十年间,Java 并没有太多的竞争对手,但从那之后,Java 就面临一些竞争了,那么有竞争是一件好事儿还是坏事儿呢?

2. 竞争加剧带来的好处

正如我们在前文所述,Java 在早期并没有太多改变,这很可能是因为没有改变的必要,尽管它远远称不上完美,但也已经得到了广泛的使用。

但是,新的竞争者出现了,这些更现代的语言带来了新的特性,解决了长期困扰 Java 开发人员的一些痛点。

举例来说,我们看一下 Scala 语言,自 2009 年以来,Scala 变得越来越受欢迎,开发人员欢迎这种新的函数式风格,它能够让他们的编码更灵活,也能安全轻松地编写出并行代码。我们可以在下面的谷歌趋势图中看到这个趋势:

Java 的新未来:逐渐“Kotlin 化”

图片来源:theboreddev.com

那 Oracle 对这种新趋势的反应是什么呢?那就是在 2014 年发布了 Java Lambdas 和 Streams。很多人都认为当时这是 Java 击败 Scala 的最大举措。目前行业中每个人都能感受到最近几年 Scala 因为缺少流所遭受的损失。

JVM 领域中存在竞争者的另外一个好处就是 JIT 编译器和 JVM 本身正在经历的持续改进。现在,有更多的人对 JVM 优化和性能改善感兴趣。所以,竞争对每个人都是好事。

Kotlin 是这个领域最新的竞争者。Kotlin 非常重要,因为它在一定程度上为 Oracle 指明了前进方向。Kotlin 表明它能够保持 Java 好的组成部分,同时创建一个更简洁和快速的编码语言。

如果看一下谷歌趋势的话,我们能够看到在过去几年间,Kotlin 的流行程度:

Java 的新未来:逐渐“Kotlin 化”

图片来源:theboreddev.com

从图中可以看出,Kotlin 迅速变得非常流行,但是在最近几年,它似乎稳定了下来。

Oracle 很好地记录了业界对 Kotlin 的反应,如果你看一下 JDK 15 的发布说明,就会发现 Java 的一些新特性基本上就是对 Kotlin 变更的复制。例如,新的 Java record、文本块(使用三个引号的多行字符串)以及 switch 语句(或多或少借鉴了 Kotlin 的 when 语句)。我们可以通过该地址查阅 JDK 15 的发布说明。

刚才提到的这些,我都将其称为“Java 的 Kotlin 化(Kotlinisation)”。Kotlin 通过成为 Java 有史以来最强的竞争对手,为 Java 指明了发展的方向。在我看来,Kotlin 是我见过的唯一一种能够战胜 Java,并成为行业领导者的语言。

3. Java 的“Kotlin”化

Java 的一些新特性主要在可读性方面进行了增强,并改善了其主要的一个弱点,那就是繁琐性。我们可以断言,它与某些 Kotlin 的特性具有相似性。

需要注意,这些特性大多数都还处于特性预览阶段,这意味着你安装 JDK 14 或 JDK 15 时,默认不会启用这些特性。

Java 特性预览指的是一些在发布版本中包含、但默认禁用的特性。在发行版中包含它们仅仅是为了收集社区开发人员的反馈,因此它们更经常发生变更,这也是为何不推荐在生产环境代码中使用它们的原因。

要在编译时启用这些特性,我们需要执行如下代码:

javac --enable-preview --release 14 

如果你想要在运行时启用它们的话,如下执行如下代码:

Javajava --enable-preview YourClass 

当然,你也可以在 IDE 中启用它们,但是不要在你的新项目中默认启用预览功能。

我们看一看这些变更,在未来的 Java 版本中,它们会对我们的编码产生更大的影响。

Java record

Java record 是我们长期以来一直要求的一项特性,我相信你早就多次遇到这样的场景了,那就是极不情愿地实现 toString、hashCode、equals 方法以及每个字段的 getter(在这里,我假设你已经不再实现 setter 了,还记得我们在“Java 中新的并发模型”一文中所讨论的不可变性吗?)。

Kotlin 提供了数据类(data class)来解决这个问题,Java 也通过发布 record 类来解决了这个问题,同样的问题,Scala 是通过 case 类来解决的。

这些类的主要目的是在对象中保存不可变的数据。让我们通过一个示例来看看它在 Java 中能够带来多么好的效果。我们实例化并对比 Employee 类,需要编写这么多的代码:

package com.theboreddev.java14; 
 
import java.util.Objects; 
 
public class Employee { 
 
private final String firstName; 
 
private final String surname; 
 
private final int age; 
 
private final Address address; 
 
private final double salary; 
 
public Employee(String firstName, String surname, int age, Address address, double salary) { 
 
this.firstName = firstName; 
 
this.surname = surname; 
 
this.age = age; 
 
this.address = address; 
 
this.salary = salary; 
 
} 
 
public String getFirstName() { 
 
return firstName; 
 
} 
 
public String getSurname() { 
 
return surname; 
 
} 
 
public int getAge() { 
 
return age; 
 
} 
 
public Address getAddress() { 
 
return address; 
 
} 
 
public double getSalary() { 
 
return salary; 
 
} 
 
@Override 
 
public boolean equals(Object o) { 
 
if (this == o) return true; 
 
if (o == null || getClass() != o.getClass()) return false; 
 
Employee employee = (Employee) o; 
 
return age == employee.age && 
 
Double.compare(employee.salary, salary) == 0 && 
 
Objects.equals(firstName, employee.firstName) && 
 
Objects.equals(surname, employee.surname) && 
 
Objects.equals(address, employee.address); 
 
} 
 
@Override 
 
public int hashCode() { 
 
return Objects.hash(firstName, surname, age, address, salary); 
 
} 
 
@Override 
 
public String toString() { 
 
return "Employee{" + 
 
"firstName='" + firstName + '\'' + 
 
", surname='" + surname + '\'' + 
 
", age=" + age + 
 
", address=" + address + 
 
", salary=" + salary + 
 
'}'; 
 
} 
 
} 

它所包含的 Address 对象如下所示:

package com.theboreddev.java14; 
 
import java.util.Objects; 
 
public class Address { 
 
private final String firstLine; 
 
private final String secondLine; 
 
private final String postCode; 
 
public Address(String firstLine, String secondLine, String postCode) { 
 
this.firstLine = firstLine; 
 
this.secondLine = secondLine; 
 
this.postCode = postCode; 
 
} 
 
public String getFirstLine() { 
 
return firstLine; 
 
} 
 
public String getSecondLine() { 
 
return secondLine; 
 
} 
 
public String getPostCode() { 
 
return postCode; 
 
} 
 
@Override 
 
public boolean equals(Object o) { 
 
if (this == o) return true; 
 
if (o == null || getClass() != o.getClass()) return false; 
 
Address address = (Address) o; 
 
return Objects.equals(firstLine, address.firstLine) && 
 
Objects.equals(secondLine, address.secondLine) && 
 
Objects.equals(postCode, address.postCode); 
 
} 
 
@Override 
 
public int hashCode() { 
 
return Objects.hash(firstLine, secondLine, postCode); 
 
} 
 
@Override 
 
public String toString() { 
 
return "Address{" + 
 
"firstLine='" + firstLine + '\'' + 
 
", secondLine='" + secondLine + '\'' + 
 
", postCode='" + postCode + '\'' + 
 
'}'; 
 
} 
 
} 

为了完成一件简单的事情,我们写了太多的代码,对不对?

接下来,我们看一下使用新的 Java record 之后,代码会是什么样子:

public record EmployeeRecord(String firstName, String surname, 
 
int age, AddressRecord address, double salary) { 
 
} 

再看一下 Address 类:

public record AddressRecord(String firstLine, String 
 
secondLine, String postCode) { 
 
} 

这和我们前面所编写的一大堆代码是同样的效果,我们不得不承认:这非常棒!从要保存的代码数量和简洁性方面都是如此。

现在我们看看新的 switch 语句有什么不同。

改善 switch 语句

新的 switch 语句解决了在 Java 中使用 switch 语句的一些固有问题。我们一直以来都被教导应该避免使用 switch 语句,因为它们很容易出错并会导致代码重复。举例来说,我们很容易遇到某个 case 条件覆盖不到的场景。

新的 switch 语句解决了这个问题,因为如果我们的 switch 语句没有涵盖我们传递给它的领域类型的所有范围,它就无法编译通过。

为了阐述该例子,我们使用 Java 创建一个 DayOfTheWeek 枚举:

public enum DayOfTheWeek { 
 
MONDAY, 
 
TUESDAY, 
 
WEDNESDAY, 
 
THURSDAY, 
 
FRIDAY, 
 
SATURDAY, 
 
SUNDAY 
 
} 

我们需要 switch 语句告诉我们每周的某一天所对应的位置。看一下通过 Java 11 该怎么实现:

final DayOfTheWeek dayOfTheWeek = DayOfTheWeek.THURSDAY; 
 
int position = 0; 
 
switch (dayOfTheWeek) { 
 
case MONDAY: 
 
position = 1; 
 
break; 
 
case TUESDAY: 
 
position = 2; 
 
break; 
 
case WEDNESDAY: 
 
position = 3; 
 
break; 
 
case THURSDAY: 
 
position = 4; 
 
break; 
 
case FRIDAY: 
 
position = 5; 
 
break; 
 
case SATURDAY: 
 
position = 6; 
 
break; 
 
case SUNDAY: 
 
position = 7; 
 
break; 
 
} 
 
System.out.println("Day " + dayOfTheWeek + " is in position " + position + " of the week"); 

使用原来的 switch 语句时,我们必须要使用一个变量,而且如果我们遗漏了一周中的某一天,代码也能编译通过。这就是 switch 语句的问题之一,非常容易出错。

Java 14 会怎样改善这种情况呢?我们快速看一下:

final DayOfTheWeek dayOfTheWeek = DayOfTheWeek.THURSDAY; 
 
int position = switch (dayOfTheWeek) { 
 
case MONDAY -> 1; 
 
case TUESDAY -> 2; 
 
case WEDNESDAY -> 3; 
 
case THURSDAY -> 4; 
 
case FRIDAY -> 5; 
 
case SATURDAY -> 6; 
 
case SUNDAY -> 7; 
 
}; 
 
System.out.println("Day " + dayOfTheWeek + " is in position " + position + " of the week"); 

我们可以看到,新的 switch 语句可以用作表达式,而不仅仅是语句。

这样带来的结果就是更加简洁,也更具有表述性,这就足以说服我们使用它了。但是,现在的 switch 还有一个重要改善,那就是如果在 switch 中没有涵盖所有 case 的话,它将无法编译通过。它会显示如下错误:

Error:(9, 24) java: the switch expression does not cover all 
 
possible input values 

现在,我们不会在 switch 语句中遗漏 case 了,这是一项非常棒的特性。

这非常类似于 Kotlin 的 when 语句,你可以通过该地址了解该语句的更多信息。

接下来,我们看一下文本块。

文本块

你有没有遇到过将一个大的 blob JSON 赋值给 Java 变量的场景?你是否也受够了这种丑陋的代码?Java 将会引入多行字符串特性,我们可以通过将它们封装在三重引号中来定义它们。当这个功能被正式发布后,定义多行长字符串会更加容易。

我们来看一下两种模式的差异。假设我们想要将一个格式化后的 JSON 存储到一个变量中,那么丑陋的代码如下所示:

final String text = "{\"widget\": {\n" + 
 
" \"debug\": \"on\",\n" + 
 
" \"window\": {\n" + 
 
" \"title\": \"Sample Konfabulator Widget\",\n" + 
 
" \"name\": \"main_window\",\n" + 
 
" \"width\": 500,\n" + 
 
" \"height\": 500\n" + 
 
" },\n" + 
 
" \"image\": { \n" + 
 
" \"src\": \"Images/Sun.png\",\n" + 
 
" \"name\": \"sun1\",\n" + 
 
" \"hOffset\": 250,\n" + 
 
" \"vOffset\": 250,\n" + 
 
" \"alignment\": \"center\"\n" + 
 
" },\n" + 
 
" \"text\": {\n" + 
 
" \"data\": \"Click Here\",\n" + 
 
" \"size\": 36,\n" + 
 
" \"style\": \"bold\",\n" + 
 
" \"name\": \"text1\",\n" + 
 
" \"hOffset\": 250,\n" + 
 
" \"vOffset\": 100,\n" + 
 
" \"alignment\": \"center\",\n" + 
 
" \"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n" + 
 
" }\n" + 
 
"}} "; 

可以更容易地编写整洁的代码了:

final String multiLineText = """ 
 
{"widget": { 
 
"debug": "on", 
 
"window": { 
 
"title": "Sample Konfabulator Widget", 
 
"name": "main_window", 
 
"width": 500, 
 
"height": 500 
 
}, 
 
"image": {\s 
 
"src": "Images/Sun.png", 
 
"name": "sun1", 
 
"hOffset": 250, 
 
"vOffset": 250, 
 
"alignment": "center" 
 
}, 
 
"text": { 
 
"data": "Click Here", 
 
"size": 36, 
 
"style": "bold", 
 
"name": "text1", 
 
"hOffset": 250, 
 
"vOffset": 100, 
 
"alignment": "center", 
 
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" 
 
} 
 
}} 
 
"""; 

我觉得这样好太多了。这也是 Kotlin 所支持的,可以在这里的类型定义中找到。

总之,我们能看到 Java 从它的竞争对手之一,也就是 Kotlin,那里“继承”了许多方案来解决自己的问题。我们不知道这次 Oracle 在对抗 Kotlin 的崛起方面是否及时做出了正确的反应,或许这有点太晚了。但我个人认为 Java 正在朝着正确的方向前进,尽管这些变化是由它的竞争对手以某种方式触发的,而且可能来得有点迟了。

如前所述,如果这篇文章激发了你学习 Kotlin 语言的兴趣,我建议你阅读“Kotlin in Action”,对于 Java 开发人员来说,这是一门很棒的 Kotlin 入门图书。

4. 结论

相关推荐