从 Java 8 迁移的 20 个理由
那些不愿满足生活需求的人,终将自食其果。如果你不想改变,你就会被抛在后面。如果你不想改变,你就会被科技时代淘汰。如果你不喜欢改变,你就会被淘汰。你无法在这个快速变化的世界里生存。生活需要改变。
2019 年 11 月 25 日更新:修复了 Oracle Java 8 支持、Java 8 中的 Graal 支持中的错误,并添加了预览功能的警告。
从 1995 年 Java 的第一个 Beta 版本发布到 2006 年底,该语言大约每隔两年就会发布一次新版本。但在 2006 年底 Java 6 发布后,开发人员不得不等待近五年才能获得新版本。Java 8 和 Java 9 的发布速度同样缓慢,每次发布都需要等待近三年和三年半的时间。
Java 7、8 和 9 版本都对 Java 生态系统的 API、垃圾收集器、编译器等进行了重大更改。Oracle 首席 Java 架构师Mark Reinhold承诺,Java 未来版本将以 6 个月的快速发布周期发布,并穿插长期支持 (LTS) 版本。到目前为止,他的团队已经兑现了这一承诺——Java 10、11、12 和 13 版本(平均)每 6 个月发布一次,平均发布间隔为 13.5 天(两周在朋友之间算什么?)。
最新的 LTS 版本是 Java 11,发布于 2018 年 9 月,在下一个 LTS 版本发布(2021 年 9 月)之前将提供免费的公共更新,并且支持将延长至 2026 年 9 月。Reinhold的新愿景是每三年(每六个版本)发布一个 Java 的 LTS 版本,因此下一个 LTS 版本将是 Java SE 17,暂定于 2021 年 9 月发布。这允许从旧 LTS 版本到新 LTS 版本的长期支持过渡期。
请注意,Oracle JDK、OpenJDK 和 AdoptOpenJDK 的发布日期和支持周期之间存在细微但显著的差异,我将在后续文章中介绍。
虽然 Java 开发者不如 Python 用户那么糟糕——去年,相当一部分 Python 用户还在使用 8 年前的*已退役*版本的 Python 进行编程,但到目前为止,我们仍然没有从 Java 8 迁移,尽管 Java 9 已经问世两年多了。有很多充分的理由让你考虑从现在使用的 Java 版本迁移到(至少)Java 11,包括……
目录
- Oracle 不再为 Java 8 提供免费支持
jshell
,Java REPL(Java 9)- 模块和链接(Java 9)
- 改进的 Javadoc(Java 9)
Collection
不可变工厂方法(Java 9)Stream
改进(Java 9)- 多版本
jar
(Java 9) private
interface
方法(Java 9)- GraalVM,一个新的 Java 虚拟机(Java 9/10)
- 局部变量类型推断(Java 10)
- 不可修改的
Collection
增强功能(Java 10) - 容器感知(Java 10)
- 单一源文件启动(Java 11)
switch
表达式——迈向模式匹配的一步(Java 12)teeing
Collectors
(Java 12)- 多行文本块(Java 13(预览版))
- 带有 Project Metropolis 的 Java-on-Java 编译器(Java 14+)
- Project Amber(Java 14+)中的流类型、匿名变量、数据类和密封类型
Fiber
Project Loom(Java 14+)中的协程、尾调用优化和轻量级用户模式- Valhalla 项目(Java 14+)中的值类型、泛型特化和具体化泛型
#1. Oracle 不再为 Java 8 提供免费支持
尽管AdoptOpenJDK将至少在 2023 年 9 月之前为其 Java 8 版本提供免费的公共更新,但 Oracle 已经停止了对其 JDK 的免费支持。“扩展支持”将持续到 2025 年 3 月,您可以通过购买 Oracle Java 订阅来获得。
#2. jshell
Java REPL(Java 9)
读取-求值-打印循环 (REPL)几乎已经成为现代编程语言的必备功能。Python 有,Ruby 有,Java(从 JDK 9 开始)也有。Java REPLjshell
是尝试一些小代码片段并实时获得反馈的好方法:
$ jshell
| Welcome to JShell -- Version 11.0.2
| For an introduction type: /help intro
jshell> var x = List.of(1, 2, 3, 4, 5)
x ==> [1, 2, 3, 4, 5]
jshell> x.stream().map(e -> (e + " squared is " + e*e)).forEach(System.out::println)
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25
#3. 模块和链接(Java 9)
Java 9 还引入了模块,它是高于包的组织级别。如果您安装了 Java 9+,即使您不知道,您也已经在使用模块了。您可以使用以下命令检查哪些模块可用:
$ java --list-modules
java.base@11.0.2
java.compiler@11.0.2
java.datatransfer@11.0.2
...
jdk.unsupported.desktop@11.0.2
jdk.xml.dom@11.0.2
jdk.zipfs@11.0.2
模块有几个好处,包括:与新的 Java 链接器结合使用时可以减小 Java 应用程序的大小(通过仅包含应用程序所需的模块和子模块);允许使用私有包(封装在模块中);以及快速失败(如果您的应用程序依赖于某个模块,而该模块在您的系统上不可用)。这可以防止当一段代码尝试访问系统中不存在的包中定义的方法时发生运行时错误。
网上有很多优秀的教程,讲解如何构建和配置模块。快来模块化吧!
#4. 改进的 Javadoc(Java 9)
从 Java 9 开始,像“java string class”这样的 Google 搜索已成为过去。全新改进的 Javadoc 不仅易于搜索,还兼容 HTML5,并兼容新的模块层次结构。点击此处查看JDK 8和JDK 9 Javadocs之间的区别。
使用我制作的这个小型 Chrome 插件,可以直接从多功能框搜索 API 的多个版本,使搜索 Oracle 的 Javadoc 变得比以往更加简单。
#5.Collection
不可变工厂方法(Java 9)
在 Java 9 之前,快速创建小的、不可修改的Set
预定义值的最简单方法是这样的:
Set<String> seasons = new HashSet<>();
seasons.add("winter");
seasons.add("spring");
seasons.add("summer");
seasons.add("fall");
seasons = Collections.unmofidiableSet(seasons);
对于这么小的任务来说,代码量真是惊人。Java 9通过引入不可变工厂方法(见上文)大大简化了这种表示法:Map.of()
List.of()
Set.of()
Set<String> seasons = Set.of("winter", "spring", "summer", "fall")
需要注意的是,Collection
以这种方式创建的 s 不能包含null
值——包括Map
条目中的值。
总的来说,这是一个值得欢迎的变化,它有助于(稍微)扭转 Java 作为一种不必要的冗长语言的声誉,并减少完成诸如创建少量Set
预定义值等小任务所需的脑力负担。
#6.Stream
改进(Java 9)
在过去十年中,该Stream
APIStream
为 Java 作为一种编程语言的范围提供了(可以说)最大的增长。它或多或少地将函数式编程for
引入 Java,将许多循环转变为map()
管道。
Java 9 对 、 、 和 方法带来Stream
了iterate()
一些takeWhile()
小dropWhile()
改进ofNullable()
。
Stream.iterate()
允许我们递归地将函数应用于 s 流Object
(以 some 开头seed
Object
),并选择何时停止返回值。它基本上是Java 8Stream.iterate()
加上 Java 8 的功能Stream.filter()
:
jshell> Stream.iterate("hey", x -> x.length() < 7, x -> x + "y").forEach(System.out::println)
hey
heyy
heyyy
heyyyy
新的takeWhile()
和dropWhile()
方法本质上是对应用过滤器Stream
,分别接受或拒绝所有值,直到满足某些条件:
jshell> IntStream.range(0, 5).takeWhile(i -> i < 3).forEach(System.out::println)
0
1
2
jshell> IntStream.range(0, 5).dropWhile(i -> i < 3).forEach(System.out::println)
3
4
Java 9 还拉近了Optional
和Stream
之间的距离,提供了Stream.ofNullable()
和Optional.stream()
方法,使得使用Stream
和Optional
值变得轻而易举:
jshell> Optional.ofNullable(null).stream().forEach(System.out::println)
jshell> Optional.of(1).stream().forEach(System.out::println)
1
jshell> Stream.ofNullable(null).forEach(System.out::println)
jshell> Stream.ofNullable(1).forEach(System.out::println)
1
Java 尚未提供功能齐全的列表推导式接口,因此开发者们绞尽脑汁地尝试各种解决方案。希望 Java 的下一个 LTS 版本能够包含真正的列表推导式,就像 Haskell 中那样:
List.comprehension(
Stream<T> input,
Predicate<? super T>... filters)
jshell> // NOT ACTUAL JAVA CODE
jshell> List.comprehension(
...> Stream.iterate(1, i -> ++i), // 1, 2, 3, 4, ...
...> i -> i%2 == 0, // 2, 4, 6, 8, 10, ...
...> i -> i > 7, // 8, 10, 12, 14, ...
...> i -> i < 13 // 8, 10, 12
...> ).take(3).forEach(System.out::println)
8
10
12
Java 已经Stream
通过iterate()
方法提供了无限个 s(只需使用不带过滤器的Java 8iterate()
即可),但只要有一个元素不符合filter
,Stream
就会被截断。为了实现真正的列表推导,只需将不符合过滤器的元素从输出中删除,而无需终止Stream
。
我们可以梦想!
#7. 多版本jar
(Java 9)
Java 9 的另一个令人兴奋的特性是能够创建多版本jar
文件。简而言之,这意味着您的包(或模块)可以包含针对每个 Java 9 及更高版本的特定实现。因此,如果客户端计算机上安装了某个类,则可以为其加载一个针对 Java 9 的特定版本。
您所要做的就是在文件Multi-Release: true
中META-INF/MANIFEST.MF
指定,jar
然后在目录中包含不同的类版本META-INF/versions
:
jar root /
- Foo.class
- Bar.class
- META-INF
- MANIFEST.MF
- versions
- 9
- Foo.class
- 10
- Foo.class
在上面的示例中,如果jar
在安装了 Java 10 的计算机上使用,则Foo.class
引用/META-INF/versions/10/Foo.class
。否则,如果有 Java 9 可用,则Foo.class
引用/META-INF/versions/9/Foo.class
。如果目标计算机上未安装这两个 Java 版本,则/Foo.class
使用默认版本。
如果您目前还在使用 Java 8,但想为将来切换到新版本做好准备,那么多版本jar
是您的最佳选择。实施它们不会有任何损失!
#8.private
interface
方法(Java 9)
Java 8在sdefault
中引入了方法interface
,这对 DRY(不要重复自己)软件开发来说是一个福音。您不再需要在单个 s 的多个实现中重新定义相同的方法interface
。相反,您可以在 s 中定义带有默认主体的方法,interface
任何实现该 s 的类都可以继承该方法interface
。
interface MyInterface {
default void printSquared (int n) {
System.out.println(n + " squared is " + n*n);
}
default void printCubed (int n) {
System.out.println(n + " cubed is " + n*n*n);
}
}
public class MyImplementation implements MyInterface { }
我们可以像这样使用此类jshell
:
jshell> var x = new MyImplementation()
x ==> MyImplementation@39c0f4a
jshell> x.printSquared(3)
3 squared is 9
jshell> x.printCubed(3)
3 cubed is 27
Java 9 进一步改进了interface
s,允许private
在 s 内部使用方法。这意味着我们可以进一步提高代码复用率,尤其是在这些默认方法实现之间,而无需用户访问这些“辅助”方法:
interface MyInterface {
private void printHelper (String verb, int n, int pow) {
System.out.printf("%d %s is %d%n", n, verb, (int) Math.pow(n, pow));
}
default void printSquared (int n) {
printHelper("squared", n, 2);
}
default void printCubed (int n) {
printHelper("cubed", n, 3);
}
}
public class MyImplementation implements MyInterface { }
我们使用这个重新实现中的方法的MyInterface
方式与上面完全相同,但是方法体中的重复代码现在被提取到一个简短的“辅助”方法中。通过减少复制粘贴的代码量,我们可以更interface
轻松地维护。
#9. GraalVM,一个新的 Java 虚拟机(Java 9/10)
GraalVM(发音类似于“crawl”,但“g”的发音是硬的,而不是“c”)是由 Oracle 基于 HotSpot 和 OpenJDK 创建的新型 Java 虚拟机和开发工具包。
Graal 的开发旨在通过尝试匹配原生(编译为机器码)语言的速度来提高 Java 应用程序的性能。GraalVM 与其他 Java 虚拟机主要有两点不同:
- 允许提前(AOT)编译
- 支持多语言编程
大多数 Java 开发者都知道,Java 会编译为 Java 字节码,之后 Java 虚拟机会读取字节码,并将其转换为适用于用户机器的特定处理器代码。这两步编译过程正是 Java “一次编写,随处运行”理念的部分原因——Java 程序员无需担心特定机器架构的具体实现。只要程序在自己的机器上运行良好,那么在其他任何机器上也都能运行,只要机器上安装了 Java 运行时环境 (JRE)。
请注意,此模型只是将特定于体系结构的细节从 Java 程序员推给了 JVM 工程师。特定于机器的代码仍然需要编写,但对于普通的 Java 开发人员来说,这些代码是隐藏的。当然,这就是为什么 Windows、Mac 和 Linux 有不同版本的 JDK 的原因。
GraalVM 将这两个步骤结合起来,生成机器原生镜像——针对虚拟机所运行的特定架构创建的二进制代码。这种从字节码到机器语言的提前编译意味着 GraalVM 生成的是二进制可执行文件,无需经过 JVM 即可立即运行。
这并非新概念,因为像 C 这样的语言一直以来都会编译为特定于机器的二进制代码,但对于 Java 生态系统来说,这却是一个新概念。(或者说,比较新,因为 Android Runtime 自 2013 年左右就开始使用 AOT 编译了。)使用 GraalVM 进行 AOT 编译可以缩短启动时间,并提高 JIT 编译代码的性能。
但真正让 GraalVM 有别于其他 Java VM 的是,Graal 是一个多语言VM:
const express = require('express');
const app = express();
app.listen(3000);
app.get('/', function(req, res) {
var text = 'Hello World!';
const BigInteger = Java.type('java.math.BigInteger');
text += BigInteger.valueOf(2).pow(100).toString(16);
text += Polyglot.eval('R', 'runif(100)')[0];
res.send(text);
})
得益于Truffle 语言实现框架, Graal 实现了Java、JavaScript、R、Python、Ruby 和 C 语言之间的零开销互操作性。用任何一种语言编写的代码都可以在用任何一种语言编写的程序中运行。您可以编译一个调用 Python 代码的 Ruby 程序,或者编译一个使用 C 语言库的 Java 程序。为了让所有这些语言能够正确地相互通信,我们投入了大量的工作,而最终的结果几乎令人难以置信。
#10. 局部变量类型推断(Java 10)
var
Java 10 使用以下关键字继续对抗样板代码:
jshell> var x = new ArrayList<Integer>();
x ==> []
jshell> x.add(42)
$2 ==> true
jshell> x
x ==> [42]
新var
类型允许在 Java 10 及更高版本中进行局部类型推断。这一小段新语法,就像之前的菱形运算符(<>
在 JDK 7 中定义)一样,使变量定义更加简洁。
局部类型推断意味着
var
只能在方法体或其他类似的代码块中使用。它不能用于声明实例变量或作为方法的返回类型等。
x
注意,上面的变量仍然有类型——它只是根据上下文推断出来的。当然,这意味着我们不能给 赋值一个非ArrayList<Integer>
值x
:
jshell> x = "String"
| Error:
| incompatible types: java.lang.String cannot be converted to java.util.ArrayList<java.lang.Integer>
| x = "String"
| ^------^
即使有类型推断,Java 仍然是一种静态类型语言。一旦变量被声明为特定类型,它就永远是该类型。这与 JavaScript 等语言不同,JavaScript 中的变量类型是动态的,可以逐行更改。
#11. 不可修改的Collection
增强功能(Java 10)
在 Java 中处理不可变数据是出了名的困难。当使用以下方式声明时,原始值是不可变的final
……
jshell> public class Test { public static final int x = 3; }
| created class Test
jshell> Test.x
$3 ==> 3
jshell> Test.x = 4
| Error:
| cannot assign a value to final variable x
| Test.x = 4
| ^----^
...但即使像final
原始数组这样简单的东西也不是真正不可变的:
jshell> public class Test { public static final int[] x = new int[]{1, 2, 3}; }
| replaced class Test
jshell> Test.x[1]
$5 ==> 2
jshell> Test.x[1] = 6
$6 ==> 6
jshell> Test.x[1]
$7 ==> 6
上面的关键字final
表示对象 x
是不可变的,但其内容不一定是可变的。在这种情况下,不可变性意味着x
只能引用特定的内存位置,因此我们不能执行以下操作:
jshell> Test.x = new int[]{8, 9, 0}
| Error:
| cannot assign a value to final variable x
| Test.x = new int[]{8, 9, 0}
| ^----^
如果x
不是final
,上面的代码就能正常运行(自己试试吧!)。当然,当程序员想要一个不可变的对象时,这会导致各种各样的问题,他们将其声明为final
,然后就可以高高兴兴地继续工作了。 s根本final Object
不是。final
Java 7 引入该类时Collections
,附带了一些unmodifiable...()
方法,这些方法提供了特定集合的“不可修改视图”。这意味着,如果您只能访问不可修改的视图,则无法访问诸如add()
、remove()
、set()
、put()
等方法。任何修改对象或其内容的方法都会被有效地隐藏在您的视图之外。眼不见,心不烦。
jshell> List<Integer> lint = new ArrayList<>();
lint ==> []
jshell> lint.addAll(List.of(1, 9, 0, 1))
$22 ==> true
jshell> lint
lint ==> [1, 9, 0, 1]
jshell> List<Integer> view = Collections.unmodifiableList(lint);
view ==> [1, 9, 0, 1]
jshell> view.add(8);
| Exception java.lang.UnsupportedOperationException
| at Collections$UnmodifiableCollection.add (Collections.java:1058)
| at (#25:1)
但是,如果您保留对底层对象的访问权限,则仍然可以修改它。任何有权访问不可修改视图的人都可以看到您的更改:
jshell> lint.addAll(List.of(1, 8, 5, 5))
$26 ==> true
jshell> lint
lint ==> [1, 9, 0, 1, 1, 8, 5, 5]
jshell> view
view ==> [1, 9, 0, 1, 1, 8, 5, 5]
因此,即使是“不可修改的视图”仍然可以被修改。
继 Java 9 的“不可变工厂方法”之后,Java 10 引入了更多 API 改进,使处理不可变数据更加轻松。首先是copyOf()
添加到 、 和 的新方法List
。Set
这些Map
方法可以创建各自类型的真正不可变(浅)副本:
jshell> List<Integer> nope = List.copyOf(lint)
nope ==> [1, 9, 0, 1, 1, 8, 5, 5]
jshell> nope.add(4)
| Exception java.lang.UnsupportedOperationException
| at ImmutableCollections.uoe (ImmutableCollections.java:71)
| at ImmutableCollections$AbstractImmutableCollection.add (ImmutableCollections.java:75)
| at (#32:1)
jshell> lint.set(3, 9)
$33 ==> 1
jshell> lint
lint ==> [1, 9, 0, 9, 1, 8, 5, 5]
jshell> nope
nope ==> [1, 9, 0, 1, 1, 8, 5, 5]
并且类中还有一些新toUnmodifiable...()
方法Collectors
,它们也能创建真正不可变的对象:
jshell> var lmod = IntStream.range(1, 6).boxed().collect(Collectors.toList())
lmod ==> [1, 2, 3, 4, 5]
jshell> lmod.add(6)
$38 ==> true
jshell> lmod
lmod ==> [1, 2, 3, 4, 5, 6]
jshell> var lunmod = IntStream.range(1, 6).boxed().collect(Collectors.toUnmodifiableList())
lunmod ==> [1, 2, 3, 4, 5]
jshell> lunmod.add(6)
| Exception java.lang.UnsupportedOperationException
| at ImmutableCollections.uoe (ImmutableCollections.java:71)
| at ImmutableCollections$AbstractImmutableCollection.add (ImmutableCollections.java:75)
| at (#41:1)
Java 正在一步步向更全面的不可变数据模型迈进。
#12. 容器感知(Java 10)
2006 年至 2008 年间,谷歌工程师在 Linux 内核中添加了一项很酷的新功能,称为 cgroups,即“控制组”。这项新功能可以“限制、统计和隔离一组进程的资源使用情况(CPU、内存、磁盘 I/O、网络等)。”
如果您使用过 Hadoop、Kubernetes 或 Docker 等广泛使用控制组的工具,那么这个概念可能对您来说很熟悉。如果没有限制特定进程组可用资源的能力,Docker 就无法存在。
不幸的是,Java 是在 cgroups 实现之前创建的,因此 Java 最初完全忽略了此功能。
然而,从 Java 10 开始,JVM 能够感知自身何时在容器内运行,并默认遵守该容器对其设置的资源限制。此功能也已反向移植到 JDK 8。因此,如果您选择最新的 Java 8 版本,该 JVM 也将具备容器感知能力。
换句话说,随着Java 10的发布,Docker和Java终于成为了朋友。
#13. 单一源文件启动(Java 11)
从 Java 11 开始,您不再需要在运行之前编译单个源文件——在主类中java
查看该main
方法,并在命令行上调用它时自动编译并运行代码:
// Example.java
public class Example {
public static void main (String[] args) {
if (args.length < 1)
System.out.println("Hello!");
else
System.out.println("Hello, " + args[0] + "!");
}
}
$ java Example.java
Hello!
$ java Example.java Biff
Hello, Biff!
这是对启动器的一个小而有用的改变java
,它可以更容易地(除其他外)教授那些 Java 新手,而无需引入明确编译此类小型入门程序的“仪式”。
#14.switch
表达式——迈向模式匹配(Java 12)
Java 里不是已经有switch
表达式了吗?这是什么?
jshell> int x = 2;
x ==> 2
jshell> switch(x) {
...> case 1: System.out.println("one"); break;
...> case 2: System.out.println("two"); break;
...> case 3: System.out.println("three"); break;
...> }
two
嗯,那是switch
语句,不是switch
表达式。语句指示程序流程,但本身不会求值。(例如,你不能做类似这样的事情y = switch(x) { ... }
。)而表达式则会求值得到结果。因此,表达式可以赋值给变量,也可以从函数返回,等等。
switch
表达式看起来与switch
语句略有不同。类似于上述语句的表达式可能如下所示:
String name = switch(x) {
case 1 -> "one";
case 2 -> "two";
case 3 -> "three";
default -> throw new IllegalArgumentException("I can only count to 3.");
};
System.out.println(name);
您可以看到这段代码与之前的代码片段之间存在一些差异。首先,我们使用箭头->
代替冒号:
。从语法上讲,这就是编译器switch
区分语句和表达式的方式。switch
其次,表达式代码中没有s。表达式不像 with语句那样break
有“fall-through”现象,所以分支之后不需要 s 。switch
switch
break
case
第三,除非列出的 s 是详尽无遗的,否则我们必须有一个。也就是说,编译器可以判断——对于 的任何可能值——是否存在一个可以捕获该值的语句。如果没有,我们必须有一个案例。到目前为止,在没有 的情况下穷尽所有可能情况的唯一真正方法是对或值进行操作。default
case
x
case
default
default
switch
boolean
enum
最后,我们可以将表达式赋值switch
给变量了!表达式和语句的区别switch
类似于 Java 中三元运算符? :
和if else
语句的区别:
jshell> int what = 0;
what ==> 0
jshell> boolean flag = false;
flag ==> false
jshell> if (flag) what = 2; else what = 3;
jshell> what
what ==> 3
jshell> what = flag ? 4 : 5
what ==> 5
指示if else
代码流,但不能分配给变量(不能像y = if (flag) 3 else 4
在 Java 中那样做),而三元运算符? :
定义表达式,可以分配给变量。
switch
表达式是 Java 迈向全面支持模式匹配的一步。此功能由 Amber 项目开发,我将在下文中详细介绍。需要注意的是,switch 表达式在 Java 12 和 13 中是“预览”功能,但计划在Java 14中升级为最终版本。
#15. teeing
Collectors
(Java 12)
请注意,
DoubleStream
确实有一个average()
方法。以下示例仅用于说明目的。
如果您曾经尝试在 Java 中对值进行复杂的操作,您就会知道s 只能迭代一次Stream
是多么烦人:Stream
jshell> var ints = DoubleStream.of(1, 2, 3, 4, 5)
ints ==> java.util.stream.DoublePipeline$Head@12cdcf4
jshell> var avg = ints.sum()
avg ==> 15.0
jshell> avg /= ints.count()
| Exception java.lang.IllegalStateException: stream has already been operated upon or closed
| at AbstractPipeline.evaluate (AbstractPipeline.java:229)
| at DoublePipeline.count (DoublePipeline.java:486)
| at (#19:1)
Java 12 通过引入Collectors.teeing()
(受UNIX 实用程序tee
Stream
的启发)来减轻您的痛苦,它可以“复制”一个流,允许您在合并结果之前执行两个同时的操作。
上述 Java 12 版本可能看起来像......
jshell> import static java.util.stream.Collectors.*
jshell> var ints = DoubleStream.of(1, 2, 3, 4, 5)
ints ==> java.util.stream.DoublePipeline$Head@574caa3f
jshell> ints.boxed().collect(teeing(
...> summingDouble(e -> e),
...> counting(),
...> (a,b) -> a/b
...> ))
$20 ==> 3.0
这还远远不够完美(语法有点笨拙,而且你可以看到我们需要double
使用 来装箱原始 sboxed()
以便稍后操作它们),但它朝着更灵活的Stream
s(也许最终是列表推导?)在 Java 中取得了进步。
#16. 多行文本块(Java 13(预览版))
Java 13 中目前提供的另一个很酷的功能是多行文本块。这是一个预览功能(即将推出的JDK 14中将提供修订后的预览版),因此您需要--enable-preview
在运行java
或 时传递标志jshell
:
请注意,此功能目前为预览功能,未来可能会有所变更或出现其他问题。请在正式使用此功能前阅读使用手册。
$ jshell --enable-preview
| Welcome to JShell -- Version 13.0.1
| For an introduction type: /help intro
jshell> String greetings = """
...> Hello. My name is Inigo Montoya.
...> You killed my father.
...> Prepare to die.
...> """
greetings ==> "Hello. My name is Inigo Montoya.\nYou killed my father.\nPrepare to die.\n"
多行文本块必须以连续三个双引号字符开头"""
,后跟换行符,同样必须以换行符结尾,后跟连续三个双引号字符"""
。
此功能在上述情况下可能并非非常有用,但它在执行诸如生成String
包含大量散布变量的 s 或尝试模拟String
多行连接缩进代码等操作时,可以极大地提高可读性。在早期版本的 Java 中,我们会这样写:
jshell> String html1 = "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java loves me!\"</h1>\n\t</body>\n</html>\n";
html1 ==> "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java ... h1>\n\t</body>\n</html>\n"
或者
jshell> String html2 =
...> "<html>\n" +
...> "\t<body>\n" +
...> "\t\t<h1>\"I love Java and Java loves me!\"</h1>\n" +
...> "\t</body>\n" +
...> "</html>\n";
html2 ==> "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java ... h1>\n\t</body>\n</html>\n"
...我们现在可以写得更易读:
jshell> String html3 = """
...> <html>
...> <body>
...> <h1>"I love Java and Java loves me!"</h1>
...> </body>
...> </html>
...> """
html3 ==> "<html>\n\t<body>\n\t\t<h1>\"I love Java and Java ... h1>\n\t</body>\n</html>\n"
jshell> html1.equals(html2); html2.equals(html1);
$16 ==> true
$17 ==> true
无需转义单引号或双引号,无需反复"..." +
书写,只需保留格式良好且保留空格的文本即可。多行字符串“fences”"""
也遵循代码的缩进级别。因此,如果您在方法或类中,则无需将多行文本String
与页面左侧对齐:
jshell> String bleh = """
...> hello
...> is it me you're looking for
...> """
bleh ==> "hello\nis it me you're looking for\n"
jshell> String bleh = """
...> hello
...> is it me you're looking for
...> """
bleh ==> " hello\n is it me you're looking for\n"
#17. Project Metropolis 中的 Java-on-Java 编译器(Java 14+)
2020 年 Java 编程最令人兴奋的事情,并非在于它在过去六年中取得了多大的进步,而是在于它在未来六年中可能的发展方向。因此,在最后的几点中,我想讨论一些正在进行的 Java项目,它们有望在不久的将来推出一些激动人心的新功能。
第一个项目是 Metropolis 项目,旨在用 Java 本身重写部分(或全部)JVM 功能。目前,JVM 部分(部分)是用其他语言(例如 C 和 C++)编写的(具体取决于您使用的 JVM )。该项目负责人将这种方法称为“Java on Java”,它具有以下几个优点:
- 将 Java 与对其他语言的依赖分离,这些依赖因新版本、错误修复、安全补丁等而变得复杂。
- 允许虚拟机使用当前应用于其他已编译 Java 代码的“热点”优化方案进行自我优化
- 可维护性/简化——如果 JVM 可以完全用 Java 重写,那么 JVM 架构师只需要了解 Java 本身,而不需要了解多种语言;这将使 JVM 更易于维护
C 和 C++(JVM 的部分组件目前以它们编写)凭借其“接近硬件”的特性,比 Java 更具优势。为了使完全基于 Java 的 JVM 达到与当前这些混合语言 JVM 相同的水平,可能需要在 Java 语言中添加新功能(例如值类型)。这是一个庞大的项目,包含许多细节(以及其他各种细节),所以不要指望它能很快实现。不过……JVM 世界中正在发生许多激动人心的事情!
#18. Project Amber(Java 14+)中的流类型、匿名变量、数据类和密封类型
Amber 项目是 Java API 中大量改进的代号,这些改进旨在简化语法。这些改进包括……
流式打字instanceof
Java 的instanceof
关键字检查对象是否是特定类或接口的实例,并返回boolean
结果:
jshell> ArrayList<Integer> alist = new ArrayList<>();
alist ==> []
jshell> alist instanceof List
$5 ==> true
jshell> alist instanceof ArrayList
$6 ==> true
jshell> alist instanceof Object
$7 ==> true
实际上,当instanceof
使用并返回时true
,相关对象将被明确地转换为所需类型,并用作该类型的对象:
jshell> void alertNChars (Object o) {
...> if (o instanceof String)
...> System.out.println("String contains " + ((String)o).length() + " characters");
...> else System.out.println("not a String");
...> }
| created method alertNChars(Object)
jshell> String s = "I am a banana";
s ==> "I am a banana"
jshell> Integer i = 1;
i ==> 1
jshell> alertNChars(s)
String contains 13 characters
jshell> alertNChars(i)
not a String
Amber 项目旨在通过流敏感类型(或“流类型”)稍微简化此语法,编译器可以推断instanceof
出块的含义。基本上,如果if (x instanceof C)
执行了一个块,则该对象必须是 类(或其子类)x
的实例,因此可以使用实例方法。在 Amber 项目之后,上述方法应该如下所示:C
C
C
void alertNChars (Object o) {
if (o instanceof String s)
System.out.println("String contains " + s.length() + " characters");
else System.out.println("not a String");
}
这是一个很小的变化,但它减少了一些视觉混乱,并为Java 中的模式匹配奠定了一些基础工作。
匿名 lambda 变量
某些语言允许用户通过使用单个下划线字符_
而不是参数标识符来忽略 lambda 表达式(以及其他地方)中的参数。从 Java 9 开始,单独使用下划线字符作为标识符会在编译时抛出错误,因此它已被“恢复”,现在也可以在 Java 中以这种“匿名”方式使用。
如果您只关心所提供的部分信息,而非全部信息,则可以使用“未命名”或“匿名”变量和参数。一个与上述链接中给出的示例类似的示例是,BiFunction
它接受 anInteger
和 a作为两个参数,但仅将作为一个 aDouble
返回。第二个参数()在实现中是不需要的:Integer
String
Double
BiFunction
BiFunction<Integer, Double, String> bids = (i, d) -> String.valueOf(i);
那么,我们为什么需要给第二个参数命名(d
)呢?匿名参数可以让我们简单地用 a 替换掉不需要的变量,_
然后就搞定了:
BiFunction<Integer, Double, String> bids = (i, _) -> String.valueOf(i);
目前 Java 中最接近此类型的东西可能是通配符?
泛型类型。当我们需要指定某个泛型类型参数,但完全不关心该类型具体是什么时,我们会使用通配符泛型。这意味着我们不能在代码的其他地方使用该类型。您可以将其称为“未命名”或“匿名”类型,其用法与上述类似。
数据类
Amber 项目中提议的“数据类”类似于data class
Kotlin 中的 、 Scala 中的case
类或 C# 中的(尚未实现的)record
s。本质上,他们的目标是减轻 Java 代码中可能存在的大量冗长问题。
一旦实现,数据类应该将所有这些可怕的样板代码变成......
package test;
public class Boilerplate {
public final int myInt;
public final double myDouble;
public final String myString;
public Boilerplate (int myInt, double myDouble, String myString) {
super();
this.myInt = myInt;
this.myDouble = myDouble;
this.myString = myString;
}
@Override
public int hashCode() {
final int prime = 31;
int result = prime + myInt;
long temp = Double.doubleToLongBits(myDouble);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result + ((myString == null) ? 0 : myString.hashCode());
return result;
}
@Override
public boolean equals (Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Boilerplate other = (Boilerplate) obj;
if (myInt != other.myInt) return false;
if (Double.doubleToLongBits(myDouble) !=
Double.doubleToLongBits(other.myDouble))
return false;
if (myString == null) {
if (other.myString != null) return false;
} else if (!myString.equals(other.myString))
return false;
return true;
}
@Override
public String toString() {
return "Boilerplate [myInt=" + myInt + ", myDouble=" + myDouble + ", myString=" + myString + "]";
}
}
...变成
record Boilerplate (int myInt, double myDouble, String myString) { }
newrecord
关键字会告诉编译器这Boilerplate
是一个标准数据类,我们想要简单访问public
实例变量,并且我们想要所有的标准方法:hashCode()
,,equals()
。toString()
虽然大多数现代 IDE 确实会为你生成所有这些代码,但有人认为它们根本不应该存在。阅读和理解这些非record
代码会带来太多的概念开销,而且有很多地方容易藏匿 bug,所以最好还是去掉它们。数据类才是 Java 的未来。
密封类型
在 Java 中,如果一个类被声明了final
,它就无法以任何方式被扩展。它不能被任何类型的子类所继承,无论是由用户还是 API 开发者编写的。与之相反的是,一个类没有被声明。final
这样的类可以由用户和 API 开发者扩展,并且可以根据需要创建任意数量的子类。
但是如果我们想要一个半类呢final
?假设我们有一个类,我们想将其子类化,但只允许指定次数(record
为了简洁起见,下面我将使用 s 表示子类化):
record FossilFuelCar (Make make, Model model) { }
record ElectricCar (Make make, Model model) { }
record HybridCar (Make make, Model model) { }
record FuelCellCar (Make make, Model model) { }
假设我们确定在可预见的未来我们只需要这四种汽车,并且我们想防止人们制造出伪造的汽车品种(SteamPoweredCar
?)。sealed
类型提供了一种方法:
sealed interface Car (Make make, Model model) { }
record FossilFuelCar (Make make, Model model) implements Car { }
record ElectricCar (Make make, Model model) implements Car { }
record HybridCar (Make make, Model model) implements Car { }
record FuelCellCar (Make make, Model model) implements Car { }
在这个文件的源代码中,我们大概可以定义Car
任意数量的 实现。但在这个文件之外,根本不允许添加任何新的实现。可以将其想象成类和enum
s之间的混搭——我们拥有指定数量的 实现Car
,仅此而已。
sealed
类和接口也能很好地满足Java 中成熟模式匹配架构所需的详尽性要求。迈向现代化又迈进了一步!
Fiber
#19.使用 Project Loom(Java 14+)实现协程、尾调用优化和轻量级用户模式
Project Loom 有一个主要焦点:轻量级多线程。
目前,如果用户想要用 Java 实现并发/多线程应用程序,他们需要在某种程度上使用Thread
s,即“Java 并发的核心抽象”。该java.util.concurrent
API还提供了许多额外的抽象,例如Locks
、Future
s 和Executor
s ,这些抽象可以使这些应用程序的构建更加简单。
但是 Java 的Thread
s 是在操作系统层面实现的。创建它们并在它们之间切换可能非常昂贵。而且操作系统会极大地影响允许的最大并发线程数,从而限制了这种方法的实用性。
Loom 项目的目标是创建一个Thread
类似于应用程序级的抽象,称为Fiber
。虚拟机级(而非操作系统级)多线程的概念对于 Java 来说其实由来已久——Java在 Java 1.1 中曾拥有这些“绿色线程”,但它们已被原生线程取代。而 Loom 的绿色线程强势回归。
能够Fibers
在相似的时间内创建比Thread
s 数量级更高的结果,意味着 JVM 将能够将多线程放在首位。Loom 将支持尾调用优化和延续(类似于 Kotlin 的协程),为 Java 并发编程开辟新的途径。
#20. Valhalla 项目(Java 14+)中的值类型、泛型特化和具体化泛型
我想讨论的最后一个项目是 Valhalla 项目,我认为在迄今为止讨论过的所有即将推出和提议的功能中,它可能会给 Java 生态系统带来最大的变化。Valhalla 项目提出了对 Java 的三大变革:
- 值类型
- 通用专业化
- 具体化泛型
值类型最好与引用类型相对照来理解,Java 的Collection
就是很好的例子。例如,在 中List
,元素作为连续的引用块存储在内存中。引用本身指向它们的值,而这些值可能存储在内存中完全不同的位置。List
因此,对 进行迭代需要一些工作,因为需要导航到每个引用的地址才能提取值。
另一方面,值类型数组的所有值都存储在连续的内存块中。不需要引用,因为下一个值只是位于内存中的下一个位置。一个很好的例子是 C 语言风格的数组,其中必须声明存储在数组中的数据类型以及数组的长度:
double balance[10];
需要长度,以便在代码运行时分配所需大小的连续内存块。当然,Java 已经拥有这种结构(数组)。但是值类型允许用户创建类似上述原始类型的数组,即使对于类似复合类型的struct
数据组也是如此。通过绕过现有类型的引用/取消引用Collections
,访问速度和数据存储效率将显著提升。
Java 中的值类型行为类似于类,拥有方法和字段,但访问速度却与原始类型一样快。值类型也可以用作泛型,而无需原始类型和包装类那样的装箱/拆箱开销。这就引出了 Valhalla 项目的下一个重要特性……
通用专业化
泛型特化听起来像是一个矛盾的说法,但它的意思是(简而言之)值类型(以及原始类型)可以用作泛型方法和类中的类型参数。因此,除了
List<Integer>
我们也可以
List<int>
大多数 Java 开发人员都知道,泛型参数T
、E
、K
、V
等必须是类。它们不能是原始类型。但为什么呢?
嗯,在编译时,Java 使用类型擦除将所有特定类型转换为一个超类。在 Java 中,这个超类是Object
。由于原始类型不继承自Object
,因此它们不能用作泛型类、方法等的类型参数。
DZone 上的这篇文章很好地解释了同构转换Object
(Java在编译时将所有类转换为同构转换)与异构转换(又称泛型特化),后者允许泛型中存在不相交的类型层次结构,就像Object
Java 中的 和 原始类型一样。出于向后兼容的原因,在现有的 Java API 中,您无法直接传递int
类型T
参数。相反,如果方法既接受原始类型,也接受Object
s 类型,则需要any
在泛型类型之外使用关键字 s 进行定义:
public class Box<any T> {
private T value;
public T getValue() {
return value;
}
}
具体化泛型
值类型的泛型特化意味着 JVM 在运行时能够感知到应用程序中传递的至少部分Object
类型。这与迄今为止用于引用类型的常规类型擦除过程形成了鲜明对比。那么,在 Valhalla 项目之后,Java 中的类型会被具体化吗?也许吧,至少会部分实现。
虽然引用类型不太可能在运行时提供类型信息(为了保持向后兼容性),但值类型有可能(很有可能?)能够提供类型信息。那么 Java 类型系统的未来会怎样呢?只有时间才能告诉我们答案。
以上列表只是 Java 9-13 中现有功能以及 Java 14+ 即将推出的功能的一小部分。自五年前 Java 8 发布以来,Java 作为一种语言和生态系统已经发生了巨大的变化。如果您从那时起就没有升级,那您真的错过了!
关注我:Dev.To | Twitter.com
支持我:Ko-Fi.com
感谢阅读!
文章来源:https://dev.to/awwsmm/20-reasons-to-move-on-from-java-8-1dio