Java基础系列-Java11特性解读

Java11 是 Java8 之后的一个 LTS 版本。Java8 的 LTS 将在今年到期,在 Java8 之后,Java11 就是最好的选择了。Java9 到 Java11 的新特性虽然没有 Java8 的跨度大,但在虚拟机层面有了很大的升级。通过 Benjamin 的这篇博客,我们来看看 Java11 有什么不同。


Java11 一直都陷入困境中并且依然有很多的人在生产环境中使用 Java8。这篇文章会使用例子来讲解 Java9 到 Java11 中最重要的新特性。这篇文章使用代码来讲解新特性,不会有大段的文字。

本地变量类型推断

Java 10 中新增了一个关键字 var,在声明本地变量的时候用 var 就不需要写明具体的数据类型(本地变量是指在方法中声明的变量)。

在 Java 10 之前的版本中,你需要这样声明变量:

1
String text = "Hello Java 9";

在 Java 10,可以使用 var 替代 String。编译器会根据变量的赋值去推断变量的类型。在下面这个例子中 text 的类型是 String

1
var text = "Hello Java 10";

通过 var 声明的变量依然是静态类型。不能对已经变量赋值另外一种类型。下面的这段代码会编译不通过:

1
2
var text = "Hello Java 11";
text = 23;

可以通过 final 来防止声明的 var 变量被重复赋值:

1
2
final var text = "Banana";
text = "Joe"; // 编译报错

var 变量必须赋值一个明确类型的值,对于没有赋值或者编译器无法推断类型的变量,都会编译错误。下面的代码都无法通过编译:

1
2
3
4
var a;
var nothing = null;
var lambda = () -> System.out.println("Pity!");
var method = this::someMethod;

本地类型推断在处理泛型代码时很好用。在下面的例子中,current 的类型是 Map<String, List<Integer>>,如果使用 var 来替代 Map<String, List<Integer>>,就可以少些很多样本代码:

1
2
3
4
var myList = new ArrayList<Map<String, List<Integer>>>();
for (var current : myList) {
System.out.println(current);
}

在 Java 11 中 var 也可以用于 lambda 的参数,但是需要加上 @Nullable 注解:

1
Predicate<String> predicate = (@Nullable var a ) -> true;

Tip: 在 Intellij IDEA 中,可以通过选中一个变量,同时按下 CMD/CTRL 来显示变量的真实类型(对于键盘党可以使用 CTRL + J)。

HTTP Client

Java 9 中隐藏一个处理 Http 请求的新 API,HttpClient。到 Java11 这个 API 已经很完善了,就在 JDK 的 java.net 包下。来看看这个 API 可以做点什么。

这个新的 HttpClient 可以同步或者异步使用。同步的请求将会阻塞当前线程直到响应返回。BodyHandlers 定义了期望的返回数据类型。(e.g. 字符串、字节数组或者文件):

1
2
3
4
5
6
7
var request = HttpRequest.newBuilder()
.uri(URI.create("https://winterbe.com"))
.GET()
.build();
var client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

同样的请求也可以进行异步处理。调用 sendAsync 不会阻塞当前线程并且会返回一个 CompleteFuture 来构建一个异步操作的管道。

1
2
3
4
5
6
7
var request = HttpRequest.newBuilder()
.uri(URI.create("https://winterbe.com"))
.build();
var client = HttpClient.newHttpClient();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);

可以省略 .GET(),默认的请求方法就是这个。

下面的例子通过 POST 方法给一个 URL 发送数据。BodyHandlers 也可以用来定义请求中需要发送的数据的类型,比如字符串字节数组文件或者输入流

1
2
3
4
5
6
7
8
var request = HttpRequest.newBuilder()
.uri(URI.create("https://postman-echo.com/post"))
.header("Content-Type", "text/plain")
.POST(HttpRequest.BodyPublishers.ofString("Hi there!"))
.build();
var client = HttpClient.newHttpClient();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode()); // 200

最后的这个例子演示了如何通过 BASIC-AUTH 来进行认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
var request = HttpRequest.newBuilder()
.uri(URI.create("https://postman-echo.com/basic-auth"))
.build();
var client = HttpClient.newBuilder()
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("postman", "password".toCharArray());
}
})
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode()); // 200

Collections

Java 中的容器比如 ListSetMap 已经扩展了很多新的方法。List.of 会根据参数创建一个新的不可变的 listList.copy 会创建这个 list 的不可变副本。

1
2
3
var list = List.of("A", "B", "C");
var copy = List.copyOf(List);
System.out.println(list == copy); // true

因为 list 已经是不可变的,在复制时没必要创建另一个实例,因此 lisicopy 指向的是同一个实例。然而如果想复制一个可变的对象,就会创建一个新的实例来保证对原对象的修改不会影响到复制的对象。

1
2
3
var list = new ArrayList<String>();
var copy = List.copyOf(list);
System.out.println(list == copy);

当创建不可变的 map 时候,不需要手动创建 map 对象,只需要使用 Map.of 方法交替传入 key 和 value 就可以。

1
2
var map = Map.of("A", 1, "B", 2);
System.out.println(map);

在 Java 11 中不可变容器的 API 没有变化。然而如果尝试对不可变的容器添加或者减少元素,就会抛出 java.lang.UnsupportedOperationException 异常。幸运的是 Intellij IDEA 会在你尝试修改不可变容器的时候发出一个警告。

Streams

Streams 是在 Java8 中加入的新特性,在后面的又加入了 3 个新的方法。 Stream.ofNullable 方法通过单个元素来构造流:

1
2
Stream.ofNullable(null)
.count(); // 0

dropWhiletakeWhile 方法都可以接受一个 predicate 参数来决定是否将符合条件的元素从流中清理出去。

predicate 是一个函数式编程的接口

Optionals

Optionals 也接收了一些相当有用的方法。比如现在可以将 optinals 很简单转成 stream 或者为一个空的 optional 返回另一个备用的 optional。

1
2
3
4
5
Optional.of("foo").orElseThrow();     // foo
Optional.of("foo").stream().count(); // 1
Optional.ofNullable(null)
.or(() -> Optional.of("fallback"))
.get(); // fallback

Strings

String 这个基础的类也新增了一些方法来校验空格以及计算字符串的行数。

1
2
3
4
5
6
" ".isBlank();                // true
" Foo Bar ".strip(); // "Foo Bar"
" Foo Bar ".stripTrailing(); // " Foo Bar"
" Foo Bar ".stripLeading(); // "Foo Bar "
"Java".repeat(3); // "JavaJavaJava"
"A\nB\nC".lines().count(); // 3

InputStreams

最后简单说一下 InputStream 提供了一个非常有用的方法来传输数据到 OutputStream,下面这个例子在传输原始数据的时候能经常看到。

1
2
3
4
5
6
var classLoader = ClassLoader.getSystemClassLoader();
var inputStream = classLoader.getResourceAsStream("myFile.txt");
var tempFile = File.createTempFile("myFileCopy", "txt");
try (var outputStream = new FileOutputStream(tempFile)) {
inputStream.transferTo(outputStream);
}

其他的 JVM 特性

上面的这些事我认为 Java8 到 Java11 中最有趣的新特性。但是新特性远远不止这些。下面的这些特性在最新的 Java 版本中都有:

下一步

许多人(包括我)依然在生产环境中使用 Java8。然而到 2020 年,Java8 的 TLS 就结束了。所以现在是一个迁移到 Java11 的好机会。我写了一篇迁移指南来帮助你从 Java8 迁移到 Java11。同时你也应该读一下我的 Java8Stream API 的教程来学习如何使用更时髦的开发方式。这些源码都已经发布到 GitHub 上了,随便玩(如果喜欢的话,点一下 star)。

(完)

微信公众号

© 2018 ray