概述

我知道很多公司和个人还在用 Java8,我们不妨梳理下当下的情况

  • 目前 Java 最新的 GA(General-Availability) Release 版本是 JDK 18.0.2.1
  • Java 17 LTS 是 最新长期支持版本。根据 Oracle 免费条款JDK 18JDK 17 可在生产环境中免费使用,至少在 2024 年 9 月之前
  • JDK 18 到 2022 年 9 月它将被 JDK 19 取代

日子总要过,我们也不可能抱着 Java 8 用一辈子,我们来一起看看 Java 11 的一些新玩意儿。

本文算是 Java 11 功能的小教程,没有长篇的文字,都是些短小易懂的代码,让我们沉浸在研究代码的快乐中吧。

1 局部变量类型推断

Java 10 引入了一个新的语言关键字var ,它可以在声明局部变量时选择性地替换类型信息

在 Java 10 之前,我们这样声明变量:

String text ="Hello World";

现在可以替换Stringvar. 编译器从变量的赋值中推断出正确的类型

var text = "Hello World";

注意:声明的变量**var**仍然是静态类型的。不能将不兼容的类型重新分配给此类变量,比如下面的代码就无法编译通过

var text = "Hello World";
text = 123;

当然 你还可以final与 结合使用var来禁止用另一个值重新分配变量:

final var text = "Hello World";
text = "hello";   // Cannot assign a value to final variable 'text'

当编译器无法推断变量的正确类型时,也var不允许使用。以下所有代码示例都会导致编译器错误:

var a;
var nothing = null;
var lambda = () -> System.out.println("Joe!");
var method = this::someMethod;

**有什么直接的好处? **

比如有一个相当冗长的类型Map<String, List<Integer>>,可以将其简化为单个var关键字,从而避免您输入一坨类型:

var myList = new ArrayList<Map<String, List<Integer>>>();

for (var current : myList) {
    // current is infered to type: Map<String, List<Integer>>
    System.out.println(current);
}

从 Java 11 开始,var关键字也允许用于 lambda 参数,这使你能够为这些参数添加注释:

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

提示:在 Intellij IDEA 中,您可以将鼠标悬停在变量上,同时按住CMD/CTRL以显示变量的推断类型

2 Http Client

Java 9 引入了一个新的孵化的HttpClientAPI 来处理 HTTP 请求。从 Java 11 开始,这个 API 现在是最终可用的了,在包java.net 中。让我们探索一下这个 API

  • newHttpClient 可以同步或异步使用。同步请求会阻塞当前线程,直到响应可用。
  • BodyHandlers 定义响应主体的预期类型(例如字符串、字节数组或文件):
var request = HttpRequest.newBuilder()
                .uri(URI.create("http://jsonplaceholder.typicode.com/users"))
                .GET()
                .build();
  var client = HttpClient.newHttpClient();
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  System.out.println(response.body());

可以异步执行相同的请求。调用sendAsync不会阻塞当前线程,而是返回一个CompletableFuture来构造异步操作流水线。

var request = HttpRequest.newBuilder()
    .uri(URI.create("http://jsonplaceholder.typicode.com/users"))
    .build();
var client = HttpClient.newHttpClient();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println);

自己跑代码的时候注意,因为是异步的,所以你可以让主线程 sleep 一会儿,不然直接运行啥都没有

最新的这个 HttpClient 还有一些其他功能就不过多介绍了,如果你使用了 Spring 5 及以上版本,我的建议是可以直接用 WeClient ,那玩意儿谁用谁知道呀。

3 Collection

List等集合已通过新方法进行了扩展。从给定的参数创建一个新的不可变列表。创建列表的不可变副本

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

注意这里 of 方法返回的是不可变类型,我们看下源码:

static <E> List<E> of(E e1, E e2, E e3) {
        return new ImmutableCollections.ListN<>(e1, e2, e3);
 }

因为list已经是不可变的,所以实际上不需要创建列表实例的副本,因此listcopy是同一个实例。但是,如果你想复制一个可变列表,copy 则返回一个新实例,因此可以保证在改变原始列表时没有副作用

var list = new ArrayList<String>();
var copy = List.copyOf(list);
System.out.println(list == copy);   // false

Map 的 of 方法方便我们直接构造:

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

注意这里仍然返回的是不可变类型,关于不可变集合的了解可以参考我之前的一篇文章 《 跟着 Guava 学 Java 之 不可变集合》

如果你修改了不可变集合会抛出 java.lang.UnsupportedOperationException 异常,IDEA 也会有相应的警告给你标明哪里出了问题。

4 Stream

流是在 Java 8 中引入的,现在接收三个新方法。Stream.ofNullable从单个元素构造一个流:

Stream.ofNullable(null).count()   // 0

dropWhiletakeWhile 是确定要从流中放弃哪些元素。

Stream.of(1, 2, 3, 2, 1)
    .dropWhile(n -> n < 3)
    .collect(Collectors.toList());  // [3, 2, 1]

Stream.of(1, 2, 3, 2, 1)
    .takeWhile(n -> n < 3)
    .collect(Collectors.toList());  // [1, 2]
  • takeWhile() 方法使用一个断言作为参数,返回给定 Stream 的子集直到断言语句第一次返回 false。如果第一个值不满足断言条件,将返回一个空的 Stream。
  • dropWhile 方法和 takeWhile 作用相反的,使用一个断言作为参数,直到断言语句第一次返回 false 才返回给定 Stream 的子集。

所以上面

  • 第一段的意思是放弃取小于 3 的元素,直到遇到第一个不小于 3 的则把后面的全部元素收集起来
  • 第二段的意思就是从第一个元素开始收集元素,直到遇到第一个不小于 3 的元素结束

5 Optional

Optional 还接收到一些非常方便的新方法,例如,您现在可以简单地将 optional 转换为流或提供另一个 optional 作为空 optional 的后备

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

6 String

最基本的类之一String有一些辅助方法来修剪或检查空格以及流式传输字符串的行:

" ".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

注意你可能觉得 strip 和 trim 方法一样,一般使用的话差不多,但实际上他们不一样,有所区别:

  • trim() 可以去除字符串前后的半角空白字符
  • strip() 可以去除字符串前后的全角和半角空白字符

你可以试试:

String test1="测试、u0020";//半角 unicode
  System.out.println(test1.trim().length());//2
  System.out.println(test1.strip().length());//2

  String test2="测试、u3000";//全角 unicode
  System.out.println(test2.trim().length());//3
  System.out.println(test2.strip().length());//2

  String test3="测试 ";//半角空白字符
  System.out.println(test3.trim().length());//2
  System.out.println(test3.strip().length());//2

  String test4="测试 ";//全角空白字符
  System.out.println(test4.trim().length());//3
  System.out.println(test4.strip().length());//2

  String test5="测试  ";//两个半角空白字符
  System.out.println(test5.trim().length());//2
  System.out.println(test5.strip().length());//2

7 InputStream

终于有一个非常实用的方法可以将数据从 inputStream 转到 outputStream 了,不用再自己写了

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);
}

我们看源码的 transferTo 方法

public long transferTo(OutputStream out) throws IOException {
        Objects.requireNonNull(out, "out");
        long transferred = 0;
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;
        while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
            out.write(buffer, 0, read);
            transferred += read;
        }
        return transferred;
    }
}

熟悉吗? 再也不用写这破玩意儿了。

最后

当然 Java 11 的更新 中远远不止上面这些内容,还有很多功能和特性值得大家去探索,比如:

  • Flow API 的反应式编程
  • G1: Full Parallel Garbage Collector
  • ZGC: Scalable Low-Latency Garbage Collector