Java 新特性

本文最后更新于 2024年1月4日 下午

Java 新特性

Java 8

Oracle 于 2014 发布了 Java 8(jdk 1.8),诸多原因使它成为目前市场上使用最多的 jdk 版本。虽然发布距今已将近 7 年,但很多程序员对其新特性还是不够了解,尤其是用惯了 Java 8 之前版本的老程序员,比如我。

为了不脱离队伍太远,还是有必要对这些新特性做一些总结梳理。它较 jdk. 7 有很多变化或者说是优化,比如 interface 里可以有静态方法,并且可以有方法体,这一点就颠覆了之前的认知;java.util.HashMap 数据结构里增加了红黑树;还有众所周知的 Lambda 表达式等等。本文不能把所有的新特性都给大家一一分享,只列出比较常用的新特性给大家做详细讲解。更多相关内容请看官网关于 Java8 的新特性的介绍open in new window

Interface

Interface 的设计初衷是面向抽象,提高扩展性。这也留有一点遗憾,Interface 修改的时候,实现它的类也必须跟着改。

为了解决接口的修改与现有的实现不兼容的问题。新 interface 的方法可以用 defaultstatic 修饰,这样就可以有方法体,实现类也不必重写此方法。

一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。

  1. default 修饰的方法,是普通实例方法,可以用 this 调用,可以被子类继承、重写。
  2. static 修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用 Interface 调用。

我们来看一个实际的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface InterfaceNew {
static void sm() {
System.out.println("interface提供的方式实现");
}
static void sm2() {
System.out.println("interface提供的方式实现");
}

default void def() {
System.out.println("interface default方法");
}
default void def2() {
System.out.println("interface default2方法");
}
//须要实现类重写
void f();
}

public interface InterfaceNew1 {
default void def() {
System.out.println("InterfaceNew1 default方法");
}
}

如果有一个类既实现了 InterfaceNew 接口又实现了 InterfaceNew1 接口,它们都有 def(),并且 InterfaceNew 接口和 InterfaceNew1 接口没有继承关系的话,这时就必须重写 def()。不然的话,编译的时候就会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{
public static void main(String[] args) {
InterfaceNewImpl interfaceNew = new InterfaceNewImpl();
interfaceNew.def();
}

@Override
public void def() {
InterfaceNew1.super.def();
}

@Override
public void f() {
}
}

在 Java 8 ,接口和抽象类有什么区别的?

很多小伙伴认为:“既然 interface 也可以有自己的方法实现,似乎和 abstract class 没多大区别了。”

其实它们还是有区别的

  1. Interface 和 class 的区别,好像是废话,主要有:

    • 接口多实现,类单继承
    • 接口的方法是 public abstract 修饰,变量是 public static final 修饰。 Abstract class 可以用其他修饰符
  2. Interface 的方法是更像是一个扩展插件。而 abstract class 的方法是要继承的。

开始我们也提到,interface 新增 defaultstatic 修饰的方法,为了解决接口的修改与现有的实现不兼容的问题,并不是为了要替代 abstract class。在使用上,该用 abstract class 的地方还是要用 abstract class,不要因为 interface 的新特性而将之替换。

记住接口永远和类不一样。

Functional interface 函数式接口

定义:也称 SAM 接口,即 Single Abstract Method interfaces,有且只有一个抽象方法,但可以有多个非抽象方法的接口。

在 java 8 中专门有一个包放函数式接口 java.util.function,该包下的所有接口都有 @FunctionalInterface 注解,提供函数式编程。

在其他包中也有函数式接口,其中一些没有 @FunctionalInterface 注解,但是只要符合函数式接口的定义就是函数式接口,与是否有

@FunctionalInterface 注解无关,注解只是在编译时起到强制规范定义的作用。其在 Lambda 表达式中有广泛的应用。

Lambda 表达式

接下来谈众所周知的 Lambda 表达式。它是推动 Java 8 发布的最重要新特性。是继泛型 (Generics)和注解 (Annotation)以来最大的变化。

使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的_函数式编程_。

Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中。

语法格式

1
2
(parameters) -> expression 或
(parameters) ->{ statements; }

Lambda 实战

我们用常用的实例来感受 Lambda 带来的便利

替代匿名内部类

过去给方法传动态参数的唯一方法是使用内部类。比如

1. Runnable 接口

1
2
3
4
5
6
7
8
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("The runable now is using!");
}
}).start();
//用lambda
new Thread(() -> System.out.println("It's a lambda function!")).start();

2. Comparator 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
List<Integer> strings = Arrays.asList(1, 2, 3);

Collections.sort(strings, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;}
});

//Lambda
Collections.sort(strings, (Integer o1, Integer o2) -> o1 - o2);
//分解开
Comparator<Integer> comparator = (Integer o1, Integer o2) -> o1 - o2;
Collections.sort(strings, comparator);

3. Listener 接口

1
2
3
4
5
6
7
8
9
JButton button = new JButton();
button.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
e.getItem();
}
});
//lambda
button.addItemListener(e -> e.getItem());

4. 自定义接口

上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法须要重写。这就是我们上文提到的函数式接口,也就是说只要方法的参数是函数式接口都可以用 Lambda 表达式。

1
2
3
4
5
@FunctionalInterface
public interface Comparator<T>{}

@FunctionalInterface
public interface Runnable{}

我们自定义一个函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
public interface LambdaInterface {
void f();
}
//使用
public class LambdaClass {
public static void forEg() {
lambdaInterfaceDemo(()-> System.out.println("自定义函数式接口"));
}
//函数式接口参数
static void lambdaInterfaceDemo(LambdaInterface i){
i.f();
}
}
集合迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void lamndaFor() {
List<String> strings = Arrays.asList("1", "2", "3");
//传统foreach
for (String s : strings) {
System.out.println(s);
}
//Lambda foreach
strings.forEach((s) -> System.out.println(s));
//or
strings.forEach(System.out::println);
//map
Map<Integer, String> map = new HashMap<>();
map.forEach((k,v)->System.out.println(v));
}
方法的引用

Java 8 允许使用 :: 关键字来传递方法或者构造函数引用,无论如何,表达式返回的类型必须是 functional-interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LambdaClassSuper {
LambdaInterface sf(){
return null;
}
}

public class LambdaClass extends LambdaClassSuper {
public static LambdaInterface staticF() {
return null;
}

public LambdaInterface f() {
return null;
}

void show() {
//1.调用静态函数,返回类型必须是functional-interface
LambdaInterface t = LambdaClass::staticF;

//2.实例方法调用
LambdaClass lambdaClass = new LambdaClass();
LambdaInterface lambdaInterface = lambdaClass::f;

//3.超类上的方法调用
LambdaInterface superf = super::sf;

//4. 构造方法调用
LambdaInterface tt = LambdaClassSuper::new;
}
}
访问变量
1
2
3
int i = 0;
Collections.sort(strings, (Integer o1, Integer o2) -> o1 - i);
//i =3;

Lambda 表达式可以引用外边变量,但是该变量默认拥有 final 属性,不能被修改,如果修改,编译时就报错。

Stream

java 新增了 java.util.stream 包,它和之前的流大同小异。之前接触最多的是资源流,比如 java.io.FileInputStream,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何_CRUD_。

Stream 依然不存储数据,不同的是它可以检索 (Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。可以想象成是 Sql 语句。

它的源数据可以是 CollectionArray 等。由于它的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用。

流类型

  1. Stream 串行流
  2. ParallelStream 并行流,可多线程执行

常用方法

接下来我们看 java.util.stream.Stream 常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* 返回一个串行流
*/
default Stream<E> stream()

/**
* 返回一个并行流
*/
default Stream<E> parallelStream()

/**
* 返回T的流
*/
public static<T> Stream<T> of(T t)

/**
* 返回其元素是指定值的顺序流。
*/
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}


/**
* 过滤,返回由与给定predicate匹配的该流的元素组成的流
*/
Stream<T> filter(Predicate<? super T> predicate);

/**
* 此流的所有元素是否与提供的predicate匹配。
*/
boolean allMatch(Predicate<? super T> predicate)

/**
* 此流任意元素是否有与提供的predicate匹配。
*/
boolean anyMatch(Predicate<? super T> predicate);

/**
* 返回一个 Stream的构建器。
*/
public static<T> Builder<T> builder();

/**
* 使用 Collector对此流的元素进行归纳
*/
<R, A> R collect(Collector<? super T, A, R> collector);

/**
* 返回此流中的元素数。
*/
long count();

/**
* 返回由该流的不同元素(根据 Object.equals(Object) )组成的流。
*/
Stream<T> distinct();

/**
* 遍历
*/
void forEach(Consumer<? super T> action);

/**
* 用于获取指定数量的流,截短长度不能超过 maxSize 。
*/
Stream<T> limit(long maxSize);

/**
* 用于映射每个元素到对应的结果
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

/**
* 根据提供的 Comparator进行排序。
*/
Stream<T> sorted(Comparator<? super T> comparator);

/**
* 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。
*/
Stream<T> skip(long n);

/**
* 返回一个包含此流的元素的数组。
*/
Object[] toArray();

/**
* 使用提供的 generator函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。
*/
<A> A[] toArray(IntFunction<A[]> generator);

/**
* 合并流
*/
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
实战

本文列出 Stream 具有代表性的方法之使用,更多的使用方法还是要看 Api。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Test
public void test() {
List<String> strings = Arrays.asList("abc", "def", "gkh", "abc");
//返回符合条件的stream
Stream<String> stringStream = strings.stream().filter(s -> "abc".equals(s));
//计算流符合条件的流的数量
long count = stringStream.count();

//forEach遍历->打印元素
strings.stream().forEach(System.out::println);

//limit 获取到1个元素的stream
Stream<String> limit = strings.stream().limit(1);
//toArray 比如我们想看这个limitStream里面是什么,比如转换成String[],比如循环
String[] array = limit.toArray(String[]::new);

//map 对每个元素进行操作返回新流
Stream<String> map = strings.stream().map(s -> s + "22");

//sorted 排序并打印
strings.stream().sorted().forEach(System.out::println);

//Collectors collect 把abc放入容器中
List<String> collect = strings.stream().filter(string -> "abc".equals(string)).collect(Collectors.toList());
//把list转为string,各元素用,号隔开
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(","));

//对数组的统计,比如用
List<Integer> number = Arrays.asList(1, 2, 5, 4);

IntSummaryStatistics statistics = number.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : "+statistics.getMax());
System.out.println("列表中最小的数 : "+statistics.getMin());
System.out.println("平均数 : "+statistics.getAverage());
System.out.println("所有数之和 : "+statistics.getSum());

//concat 合并流
List<String> strings2 = Arrays.asList("xyz", "jqx");
Stream.concat(strings2.stream(),strings.stream()).count();

//注意 一个Stream只能操作一次,不能断开,否则会报错。
Stream stream = strings.stream();
//第一次使用
stream.limit(2);
//第二次使用
stream.forEach(System.out::println);
//报错 java.lang.IllegalStateException: stream has already been operated upon or closed

//但是可以这样, 连续使用
stream.limit(2).forEach(System.out::println);
}

延迟执行

在执行返回 Stream 的方法时,并不立刻执行,而是等返回一个非 Stream 的方法后才执行。因为拿到 Stream 并不能直接用,而是需要处理成一个常规类型。这里的 Stream 可以想象成是二进制流(2 个完全不一样的东东),拿到也看不懂。

我们下面分解一下 filter 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void laziness(){
List<String> strings = Arrays.asList("abc", "def", "gkh", "abc");
Stream<Integer> stream = strings.stream().filter(new Predicate() {
@Override
public boolean test(Object o) {
System.out.println("Predicate.test 执行");
return true;
}
});

System.out.println("count 执行");
stream.count();
}
/*-------执行结果--------*/
count 执行
Predicate.test 执行
Predicate.test 执行
Predicate.test 执行
Predicate.test 执行

按执行顺序应该是先打印 4 次「 Predicate.test 执行」,再打印「 count 执行」。实际结果恰恰相反。说明 filter 中的方法并没有立刻执行,而是等调用 count() 方法后才执行。

上面都是串行 Stream 的实例。并行 parallelStream 在使用方法上和串行一样。主要区别是 parallelStream 可多线程执行,是基于 ForkJoin 框架实现的,有时间大家可以了解一下 ForkJoin 框架和 ForkJoinPool。这里可以简单的理解它是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题。下面我们通过代码来体验一下并行流的多线程执行。

1
2
3
4
5
6
7
8
9
10
@Test
public void parallelStreamTest(){
List<Integer> numbers = Arrays.asList(1, 2, 5, 4);
numbers.parallelStream() .forEach(num->System.out.println(Thread.currentThread().getName()+">>"+num));
}
//执行结果
main>>5
ForkJoinPool.commonPool-worker-2>>4
ForkJoinPool.commonPool-worker-11>>1
ForkJoinPool.commonPool-worker-9>>2

从结果中我们看到,for-each 用到的是多线程。

小结

从源码和实例中我们可以总结出一些 stream 的特点

  1. 通过简单的链式编程,使得它可以方便地对遍历处理后的数据进行再处理。
  2. 方法参数都是函数式接口类型
  3. 一个 Stream 只能操作一次,操作完就关闭了,继续使用这个 stream 会报错。
  4. Stream 不保存数据,不改变数据源

Optional

阿里巴巴开发手册关于 Optional 的介绍open in new window 中这样写到:

防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f () { return Integer 对象},如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj. GetA (). GetB (). GetC ();一连串调用,易产生 NPE。
正例:使用 JDK 8 的 Optional 类来防止 NPE 问题。

他建议使用 Optional 解决 NPE(java.lang.NullPointerException)问题,它就是为 NPE 而生的,其中可以包含空值或非空值。下面我们通过源码逐步揭开 Optional 的红盖头。

假设有一个 Zoo 类,里面有个属性 Dog,需求要获取 Dogage

1
2
3
4
5
6
7
class Zoo {
private Dog dog;
}

class Dog {
private int age;
}

传统解决 NPE 的办法如下:

1
2
3
4
5
6
7
8
Zoo zoo = getZoo();
if(zoo != null){
Dog dog = zoo.getDog();
if(dog != null){
int age = dog.getAge();
System.out.println(age);
}
}

层层判断对象非空,有人说这种方式很丑陋不优雅,我并不这么认为。反而觉得很整洁,易读,易懂。你们觉得呢?

Optional 是这样的实现的:

1
2
3
Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).ifPresent(age ->
System.out.println(age)
);

是不是简洁了很多呢?

如何创建一个 Optional

上例中 Optional.ofNullable 是其中一种创建 Optional 的方式。我们先看一下它的含义和其他创建 Optional 的源码方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Common instance for {@code empty()}. 全局EMPTY对象
*/
private static final Optional<?> EMPTY = new Optional<>();

/**
* Optional维护的值
*/
private final T value;

/**
* 如果value是null就返回EMPTY,否则就返回of(T)
*/
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
/**
* 返回 EMPTY 对象
*/
public static<T> Optional<T> empty() {
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
/**
* 返回Optional对象
*/
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
/**
* 私有构造方法,给value赋值
*/
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
/**
* 所以如果of(T value) 的value是null,会抛出NullPointerException异常,这样貌似就没处理NPE问题
*/
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

ofNullable 方法和 of 方法唯一区别就是当 value 为 null 时,ofNullable 返回的是 EMPTY,of 会抛出 NullPointerException 异常。如果需要把 NullPointerException 暴漏出来就用 of,否则就用 ofNullable

map()flatMap() 有什么区别的?

mapflatMap 都是将一个函数应用于集合中的每个元素,但不同的是 map 返回一个新的集合,flatMap 是将每个元素都映射为一个集合,最后再将这个集合展平。

在实际应用场景中,如果 map 返回的是数组,那么最后得到的是一个二维数组,使用 flatMap 就是为了将这个二维数组展平变成一个一维数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MapAndFlatMapExample {
public static void main(String[] args) {
List<String[]> listOfArrays = Arrays.asList(
new String[]{"apple", "banana", "cherry"},
new String[]{"orange", "grape", "pear"},
new String[]{"kiwi", "melon", "pineapple"}
);

List<String[]> mapResult = listOfArrays.stream()
.map(array -> Arrays.stream(array).map(String::toUpperCase).toArray(String[]::new))
.collect(Collectors.toList());

System.out.println("Using map:");
System.out.println(mapResult);

List<String> flatMapResult = listOfArrays.stream()
.flatMap(array -> Arrays.stream(array).map(String::toUpperCase))
.collect(Collectors.toList());

System.out.println("Using flatMap:");
System.out.println(flatMapResult);
}
}

运行结果:

1
2
3
4
5
Using map:
[[APPLE, BANANA, CHERRY], [ORANGE, GRAPE, PEAR], [KIWI, MELON, PINEAPPLE]]

Using flatMap:
[APPLE, BANANA, CHERRY, ORANGE, GRAPE, PEAR, KIWI, MELON, PINEAPPLE]

最简单的理解就是 flatMap() 可以将 map() 的结果展开。

Optional 里面,当使用 map() 时,如果映射函数返回的是一个普通值,它会将这个值包装在一个新的 Optional 中。而使用 flatMap 时,如果映射函数返回的是一个 Optional,它会将这个返回的 Optional 展平,不再包装成嵌套的 Optional

下面是一个对比的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) {
int userId = 1;

// 使用flatMap的代码
String cityUsingFlatMap = getUserById(userId)
.flatMap(OptionalExample::getAddressByUser)
.map(Address::getCity)
.orElse("Unknown");

System.out.println("User's city using flatMap: " + cityUsingFlatMap);

// 不使用flatMap的代码
Optional<Optional<Address>> optionalAddress = getUserById(userId)
.map(OptionalExample::getAddressByUser);

String cityWithoutFlatMap;
if (optionalAddress.isPresent()) {
Optional<Address> addressOptional = optionalAddress.get();
if (addressOptional.isPresent()) {
Address address = addressOptional.get();
cityWithoutFlatMap = address.getCity();
} else {
cityWithoutFlatMap = "Unknown";
}
} else {
cityWithoutFlatMap = "Unknown";
}

System.out.println("User's city without flatMap: " + cityWithoutFlatMap);
}

StreamOptional 中正确使用 flatMap 可以减少很多不必要的代码。

判断 value 是否为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* value是否为null
*/
public boolean isPresent() {
return value != null;
}
/**
* 如果value不为null执行consumer.accept
*/
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}

获取 value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
* 如果value != null 返回value,否则返回other的执行结果
*/
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

/**
* 如果value != null 返回value,否则返回T
*/
public T orElse(T other) {
return value != null ? value : other;
}

/**
* 如果value != null 返回value,否则抛出参数返回的异常
*/
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
/**
* value为null抛出NoSuchElementException,不为空返回value。
*/
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}

过滤值

1
2
3
4
5
6
7
8
9
10
11
/**
* 1. 如果是empty返回empty
* 2. predicate.test(value)==true 返回this,否则返回empty
*/
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}

小结

看完 Optional 源码,Optional 的方法真的非常简单,值得注意的是如果坚决不想看见 NPE,就不要用 of()get()flatMap(..)。最后再综合用一下 Optional 的高频方法。

1
Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).filter(v->v==1).orElse(3);

Date-Time API

这是对 java.util.Date 强有力的补充,解决了 Date 类的大部分痛点:

  1. 非线程安全
  2. 时区处理麻烦
  3. 各种格式化、和时间计算繁琐
  4. 设计有缺陷,Date 类同时包含日期和时间;还有一个 java. Sql. Date,容易混淆。

我们从常用的时间实例来对比 java. Util. Date 和新 Date 有什么区别。用 java.util.Date 的代码该改改了。

Java. Time 主要类

java.util.Date 既包含日期又包含时间,而 java.time 把它们进行了分离

1
2
3
LocalDateTime.class //日期+时间 format: yyyy-MM-ddTHH:mm:ss.SSS
LocalDate.class //日期 format: yyyy-MM-dd
LocalTime.class //时间 format: HH:mm:ss

格式化

Java 8 之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void oldFormat(){
Date now = new Date();
//format yyyy-MM-dd
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String date = sdf.format(now);
System.out.println(String.format("date format : %s", date));

//format HH:mm:ss
SimpleDateFormat sdft = new SimpleDateFormat("HH:mm:ss");
String time = sdft.format(now);
System.out.println(String.format("time format : %s", time));

//format yyyy-MM-dd HH:mm:ss
SimpleDateFormat sdfdt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String datetime = sdfdt.format(now);
System.out.println(String.format("dateTime format : %s", datetime));
}

Java 8 之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void newFormat(){
//format yyyy-MM-dd
LocalDate date = LocalDate.now();
System.out.println(String.format("date format : %s", date));

//format HH:mm:ss
LocalTime time = LocalTime.now().withNano(0);
System.out.println(String.format("time format : %s", time));

//format yyyy-MM-dd HH:mm:ss
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateTimeStr = dateTime.format(dateTimeFormatter);
System.out.println(String.format("dateTime format : %s", dateTimeStr));
}

字符串转日期格式

Java 8 之前:

1
2
3
4
5
//已弃用
Date date = new Date("2021-01-26");
//替换为
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date1 = sdf.parse("2021-01-26");

Java 8 之后:

1
2
3
4
5
6
7
8
LocalDate date = LocalDate.of(2021, 1, 26);
LocalDate.parse("2021-01-26");

LocalDateTime dateTime = LocalDateTime.of(2021, 1, 26, 12, 12, 22);
LocalDateTime.parse("2021-01-26 12:12:22");

LocalTime time = LocalTime.of(12, 12, 22);
LocalTime.parse("12:12:22");

Java 8 之前 转换都需要借助 SimpleDateFormat 类,而Java 8 之后只需要 LocalDateLocalTimeLocalDateTimeofparse 方法。

日期计算

下面仅以一周后日期为例,其他单位(年、月、日、1/2 日、时等等)大同小异。另外,这些单位都在 java. Time. Temporal. ChronoUnit 枚举中定义。

Java 8 之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void afterDay(){
//一周后的日期
SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd");
Calendar ca = Calendar.getInstance();
ca.add(Calendar.DATE, 7);
Date d = ca.getTime();
String after = formatDate.format(d);
System.out.println("一周后日期:" + after);

//算两个日期间隔多少天,计算间隔多少年,多少月方法类似
String dates1 = "2021-12-23";
String dates2 = "2021-02-26";
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Date date1 = format.parse(dates1);
Date date2 = format.parse(dates2);
int day = (int) ((date1.getTime() - date2.getTime()) / (1000 * 3600 * 24));
System.out.println(dates1 + "和" + dates2 + "相差" + day + "天");
//结果:2021-02-26和2021-12-23相差300天
}

Java 8 之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void pushWeek(){
//一周后的日期
LocalDate localDate = LocalDate.now();
//方法1
LocalDate after = localDate.plus(1, ChronoUnit.WEEKS);
//方法2
LocalDate after2 = localDate.plusWeeks(1);
System.out.println("一周后日期:" + after);

//算两个日期间隔多少天,计算间隔多少年,多少月
LocalDate date1 = LocalDate.parse("2021-02-26");
LocalDate date2 = LocalDate.parse("2021-12-23");
Period period = Period.between(date1, date2);
System.out.println("date1 到 date2 相隔:"
+ period.getYears() + "年"
+ period.getMonths() + "月"
+ period.getDays() + "天");
//打印结果是 “date1 到 date2 相隔:0年9月27天”
//这里period.getDays()得到的天是抛去年月以外的天数,并不是总天数
//如果要获取纯粹的总天数应该用下面的方法
long day = date2.toEpochDay() - date1.toEpochDay();
System.out.println(date1 + "和" + date2 + "相差" + day + "天");
//打印结果:2021-02-26和2021-12-23相差300天
}

获取指定日期

除了日期计算繁琐,获取特定一个日期也很麻烦,比如获取本月最后一天,第一天。

Java 8 之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void getDay() {

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
//获取当前月第一天:
Calendar c = Calendar.getInstance();
c.set(Calendar.DAY_OF_MONTH, 1);
String first = format.format(c.getTime());
System.out.println("first day:" + first);

//获取当前月最后一天
Calendar ca = Calendar.getInstance();
ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));
String last = format.format(ca.getTime());
System.out.println("last day:" + last);

//当年最后一天
Calendar currCal = Calendar.getInstance();
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(Calendar.YEAR, currCal.get(Calendar.YEAR));
calendar.roll(Calendar.DAY_OF_YEAR, -1);
Date time = calendar.GetTime ();
System.Out.Println ("last day: " + format.Format (time));
}

Java 8 之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
Public void getDayNew () {
LocalDate today = LocalDate.Now ();
//获取当前月第一天:
LocalDate firstDayOfThisMonth = today.With (TemporalAdjusters.FirstDayOfMonth ());
// 取本月最后一天
LocalDate lastDayOfThisMonth = today.With (TemporalAdjusters.LastDayOfMonth ());
//取下一天:
LocalDate nextDay = lastDayOfThisMonth.PlusDays (1);
//当年最后一天
LocalDate lastday = today.With (TemporalAdjusters.LastDayOfYear ());
//2021 年最后一个周日,如果用 Calendar 是不得烦死。
LocalDate lastMondayOf 2021 = LocalDate.Parse ("2021-12-31"). With (TemporalAdjusters.LastInMonth (DayOfWeek. SUNDAY));
}

java. Time. Temporal. TemporalAdjusters 里面还有很多便捷的算法,这里就不带大家看 Api 了,都很简单,看了秒懂。

JDBC 和 java 8

现在 jdbc 时间类型和 java 8 时间类型对应关系是

  1. Date —> LocalDate
  2. Time —> LocalTime
  3. Timestamp —> LocalDateTime

而之前统统对应 Date,也只有 Date

时区

时区:正式的时区划分为每隔经度 15° 划分一个时区,全球共 24 个时区,每个时区相差 1 小时。但为了行政上的方便,常将 1 个国家或 1 个省份划在一起,比如我国幅员宽广,大概横跨 5 个时区,实际上只用东八时区的标准时即北京时间为准。

java. Util. Date 对象实质上存的是 1970 年 1 月 1 日 0 点( GMT)至 Date 对象所表示时刻所经过的毫秒数。也就是说不管在哪个时区 new Date,它记录的毫秒数都一样,和时区无关。但在使用上应该把它转换成当地时间,这就涉及到了时间的国际化。java. Util. Date 本身并不支持国际化,需要借助 TimeZone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//北京时间:Wed Jan 27 14:05:29 CST 2021
Date date = new Date ();

SimpleDateFormat bjSdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm: ss");
//北京时区
BjSdf.SetTimeZone (TimeZone.GetTimeZone ("Asia/Shanghai"));
System.Out.Println ("毫秒数: " + date.GetTime () + ", 北京时间: " + bjSdf.Format (date));

//东京时区
SimpleDateFormat tokyoSdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm: ss");
TokyoSdf.SetTimeZone (TimeZone.GetTimeZone ("Asia/Tokyo")); // 设置东京时区
System.Out.Println ("毫秒数: " + date.GetTime () + ", 东京时间: " + tokyoSdf.Format (date));

//如果直接 print 会自动转成当前时区的时间
System.Out.Println (date);
//Wed Jan 27 14:05:29 CST 2021

在新特性中引入了 java. Time. ZonedDateTime 来表示带时区的时间。它可以看成是 LocalDateTime + ZoneId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//当前时区时间
ZonedDateTime zonedDateTime = ZonedDateTime.Now ();
System.Out.Println ("当前时区时间: " + zonedDateTime);

//东京时间
ZoneId zoneId = ZoneId.Of (ZoneId. SHORT_IDS.Get ("JST"));
ZonedDateTime tokyoTime = zonedDateTime.WithZoneSameInstant (zoneId);
System.Out.Println ("东京时间: " + tokyoTime);

// ZonedDateTime 转 LocalDateTime
LocalDateTime localDateTime = tokyoTime.ToLocalDateTime ();
System.Out.Println ("东京时间转当地时间: " + localDateTime);

//LocalDateTime 转 ZonedDateTime
ZonedDateTime localZoned = localDateTime.AtZone (ZoneId.SystemDefault ());
System.Out.Println ("本地时区时间: " + localZoned);

//打印结果
当前时区时间: 2021-01-27 T14:43:58.735+08:00[Asia/Shanghai]
东京时间: 2021-01-27 T15:43:58.735+09:00[Asia/Tokyo]
东京时间转当地时间: 2021-01-27 T15:43:58.735
当地时区时间: 2021-01-27 T15:53:35.618+08:00[Asia/Shanghai]

小结

通过上面比较新老 Date 的不同,当然只列出部分功能上的区别,更多功能还得自己去挖掘。总之 date-time-api 给日期操作带来了福利。在日常工作中遇到 date 类型的操作,第一考虑的是 date-time-api,实在解决不了再考虑老的 Date。

总结

我们梳理总结的 java 8 新特性有

  • Interface & functional Interface
  • Lambda
  • Stream
  • Optional
  • Date time-api

这些都是开发当中比较常用的特性。梳理下来发现它们真香,而我却没有更早的应用。总觉得学习 java 8 新特性比较麻烦,一直使用老的实现方式。其实这些新特性几天就可以掌握,一但掌握,效率会有很大的提高。其实我们涨工资也是涨的学习的钱,不学习终究会被淘汰,35 岁危机会提前来临。

《Java 8 指南》中文翻译

接口的默认方法 (Default Methods for Interfaces)

Java 8 使我们能够通过使用 default 关键字向接口添加非抽象方法实现。此功能也称为虚拟扩展方法open in new window

第一个例子:

1
2
3
4
5
6
7
8
9
Interface Formula{

Double calculate (int a);

Default double sqrt (int a) {
Return Math.Sqrt (a);
}

}

Formula 接口中除了抽象方法计算接口公式还定义了默认方法 sqrt。实现该接口的类只需要实现抽象方法 calculate。默认方法 sqrt 可以直接使用。当然你也可以直接通过接口创建对象,然后实现接口中的默认方法就可以了,我们通过代码演示一下这种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Public class Main {

Public static void main (String[] args) {
// 通过匿名内部类方式访问接口
Formula formula = new Formula () {
@Override
Public double calculate (int a) {
Return sqrt (a * 100);
}
};

System.Out.Println (formula.Calculate (100)); // 100.0
System.Out.Println (formula.Sqrt (16)); // 4.0

}

}

Formula 是作为匿名对象实现的。该代码非常容易理解,6 行代码实现了计算 sqrt (a * 100)。在下一节中,我们将会看到在 Java 8 中实现单个方法对象有一种更好更方便的方法。

译者注: 不管是抽象类还是接口,都可以通过匿名内部类的方式访问。不能通过抽象类或者接口直接创建对象。对于上面通过匿名内部类方式访问接口,我们可以这样理解:一个内部类实现了接口里的抽象方法并且返回一个内部类对象,之后我们让接口的引用来指向这个对象。

Lambda 表达式 (Lambda expressions)

首先看看在老版本的 Java 中是如何排列字符串的:

1
2
3
4
5
6
7
8
List<String> names = Arrays.AsList ("peter", "anna", "mike", "xenia");

Collections.Sort (names, new Comparator<String>() {
@Override
Public int compare (String a, String b) {
return b.compareTo (a);
}
});

只需要给静态方法 Collections. Sort 传入一个 List 对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给 sort 方法。

在 Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8 提供了更简洁的语法,lambda 表达式:

1
2
3
Collections.Sort (names, (String a, String b) -> {
return b.compareTo (a);
});

可以看出,代码变得更短且更具有可读性,但是实际上还可以写得更短:

1
Collections.Sort (names, (String a, String b) -> b.compareTo (a));

对于函数体只有一行代码的,你可以去掉大括号{}以及 return 关键字,但是你还可以写得更短点:

1
names.Sort ((a, b) -> b.compareTo (a));

List 类本身就有一个 sort 方法。并且 Java 编译器可以自动推导出参数类型,所以你可以不用再写一次类型。接下来我们看看 lambda 表达式还有什么其他用法。

函数式接口 (Functional Interfaces)

译者注: 原文对这部分解释不太清楚,故做了修改!

Java 语言设计者们投入了大量精力来思考如何使现有的函数友好地支持 Lambda。最终采取的方法是:增加函数式接口的概念。“函数式接口”是指仅仅只包含一个抽象方法, 但是可以有多个非抽象方法 (也就是上面提到的默认方法)的接口。 像这样的接口,可以被隐式转换为 lambda 表达式。java. Lang. Runnablejava. Util. Concurrent. Callable 是函数式接口最典型的两个例子。Java 8 增加了一种特殊的注解 @FunctionalInterface, 但是这个注解通常不是必须的 (某些情况建议使用),只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。一般建议在接口上使用 @FunctionalInterface 注解进行声明,这样的话,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的,如下图所示

@FunctionalInterface 注解

@FunctionalInterface 注解

示例:

1
2
3
4
@FunctionalInterface
public interface Converter<F, T> {
T convert (F from);
}
1
2
3
4
// TODO 将数字字符串转换为整数类型
Converter<String, Integer> converter = (from) -> Integer.ValueOf (from);
Integer converted = converter.Convert ("123");
System.Out.Println (converted.GetClass ()); //class java. Lang. Integer

译者注: 大部分函数式接口都不用我们自己写,Java 8 都给我们实现好了,这些接口都在 java. Util. Function 包里。

方法和构造函数引用 (Method and Constructor References)

前一节中的代码还可以通过静态方法引用来表示:

1
2
3
Converter<String, Integer> converter = Integer:: valueOf;
Integer converted = converter.Convert ("123");
System.Out.Println (converted.GetClass ()); //class java. Lang. Integer

Java 8 允许您通过 :: 关键字传递方法或构造函数的引用。上面的示例显示了如何引用静态方法。但我们也可以引用对象方法:

1
2
3
4
5
Class Something {
String startsWith (String s) {
return String.ValueOf (s.charAt (0));
}
}
1
2
3
4
Something something = new Something ();
Converter<String, String> converter = something:: startsWith;
String converted = converter.Convert ("Java");
System.Out.Println (converted); // "J"

接下来看看构造函数是如何使用 :: 关键字来引用的,首先我们定义一个包含多个构造函数的简单类:

1
2
3
4
5
6
7
8
9
10
11
Class Person {
String firstName;
String lastName;

Person () {}

Person (String firstName, String lastName) {
This. FirstName = firstName;
This. LastName = lastName;
}
}

接下来我们指定一个用来创建 Person 对象的对象工厂接口:

1
2
3
interface PersonFactory<P extends Person> {
P create (String firstName, String lastName);
}

这里我们使用构造函数引用来将他们关联起来,而不是手动实现一个完整的工厂:

1
2
PersonFactory<Person> personFactory = Person:: new;
Person person = personFactory.Create ("Peter", "Parker");

我们只需要使用 Person::new 来获取 Person 类构造函数的引用,Java 编译器会自动根据 PersonFactory. Create 方法的参数类型来选择合适的构造函数。

Lambda 表达式作用域 (Lambda Scopes)

访问局部变量

我们可以直接在 lambda 表达式中访问外部的局部变量:

1
2
3
4
5
Final int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.ValueOf (from + num);

StringConverter.Convert (2); // 3

但是和匿名对象不同的是,这里的变量 num 可以不用声明为 final,该代码同样正确:

1
2
3
4
5
Int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.ValueOf (from + num);

StringConverter.Convert (2); // 3

不过这里的 num 必须不可被后面的代码修改(即隐性的具有 final 的语义),例如下面的就无法编译:

1
2
3
4
Int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.ValueOf (from + num);
Num = 3;//在 lambda 表达式中试图修改 num 同样是不允许的。

访问字段和静态变量

与局部变量相比,我们在 lambda 表达式中对实例字段和静态变量都有读写访问权限。该行为和匿名对象是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class Lambda 4 {
Static int outerStaticNum;
Int outerNum;

Void testScopes () {
Converter<Integer, String> stringConverter 1 = (from) -> {
OuterNum = 23;
Return String.ValueOf (from);
};

Converter<Integer, String> stringConverter 2 = (from) -> {
OuterStaticNum = 72;
Return String.ValueOf (from);
};
}
}

访问默认接口方法

还记得第一节中的 formula 示例吗? Formula 接口定义了一个默认方法 sqrt,可以从包含匿名对象的每个 formula 实例访问该方法。这不适用于 lambda 表达式。

无法从 lambda 表达式中访问默认方法, 故以下代码无法编译:

1
Formula formula = (a) -> sqrt (a * 100);

内置函数式接口 (Built-in Functional Interfaces)

JDK 1.8 API 包含许多内置函数式接口。其中一些接口在老版本的 Java 中是比较常见的比如:ComparatorRunnable,这些接口都增加了 @FunctionalInterface 注解以便能用在 lambda 表达式上。

但是 Java 8 API 同样还提供了很多全新的函数式接口来让你的编程工作更加方便,有一些接口是来自 Google Guavaopen in new window 库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到 lambda 上使用的。

Predicate

Predicate 接口是只有一个参数的返回布尔类型值的 断言型 接口。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非):

译者注: Predicate 接口源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Package java. Util. Function;
Import java. Util. Objects;

@FunctionalInterface
public interface Predicate<T> {

// 该方法是接受一个传入类型, 返回一个布尔值. 此方法应用于判断.
Boolean test (T t);

//and 方法与关系型运算符"&&"相似,两边都成立才返回 true
default Predicate<T> and (Predicate<? super T> other) {
Objects.RequireNonNull (other);
Return (t) -> test (t) && other.Test (t);
}
// 与关系运算符"!"相似,对判断进行取反
default Predicate<T> negate () {
Return (t) -> !Test (t);
}
//or 方法与关系型运算符"||"相似,两边只要有一个成立就返回 true
default Predicate<T> or (Predicate<? super T> other) {
Objects.RequireNonNull (other);
Return (t) -> test (t) || other.Test (t);
}
// 该方法接收一个 Object 对象, 返回一个 Predicate 类型. 此方法用于判断第一个 test 的方法与第二个 test 方法相同(equal).
static <T> Predicate<T> isEqual (Object targetRef) {
Return (null == targetRef)
? Objects::isNull
: object -> targetRef.Equals (object);
}

示例:

1
2
3
4
5
6
7
8
9
10
Predicate<String> predicate = (s) -> s.length () > 0;

Predicate.Test ("foo"); // true
Predicate.Negate (). Test ("foo"); // false

Predicate<Boolean> nonNull = Objects:: nonNull;
Predicate<Boolean> isNull = Objects:: isNull;

Predicate<String> isEmpty = String:: isEmpty;
Predicate<String> isNotEmpty = isEmpty.Negate ();

Function

Function 接口接受一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen):

译者注: Function 接口源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Package java. Util. Function;

Import java. Util. Objects;

@FunctionalInterface
public interface Function<T, R> {

//将 Function 对象应用到输入的参数上,然后返回计算结果。
R apply (T t);
//将两个 Function 整合,并返回一个能够执行两个 Function 对象功能的 Function 对象。
default <V> Function<V, R> compose (Function<? super V, ? extends T> before) {
Objects.RequireNonNull (before);
Return (V v) -> apply (before.Apply (v));
}
//
default <V> Function<T, V> andThen (Function<? super R, ? extends V> after) {
Objects.RequireNonNull (after);
Return (T t) -> after.Apply (apply (t));
}

static <T> Function<T, T> identity () {
Return t -> t;
}
}
1
2
3
Function<String, Integer> toInteger = Integer:: valueOf;
Function<String, String> backToString = toInteger.AndThen (String::valueOf);
BackToString.Apply ("123"); // "123"

Supplier

Supplier 接口产生给定泛型类型的结果。与 Function 接口不同,Supplier 接口不接受参数。

1
2
Supplier<Person> personSupplier = Person:: new;
PersonSupplier.Get (); // new Person

Consumer

Consumer 接口表示要对单个输入参数执行的操作。

1
2
Consumer<Person> greeter = (p) -> System.Out.Println ("Hello, " + p.firstName);
Greeter.Accept (new Person ("Luke", "Skywalker"));

Comparator

Comparator 是老 Java 中的经典接口, Java 8 在此之上添加了多种默认方法:

1
2
3
4
5
6
7
Comparator<Person> comparator = (p 1, p 2) -> p 1.FirstName.CompareTo (p 2. FirstName);

Person p 1 = new Person ("John", "Doe");
Person p 2 = new Person ("Alice", "Wonderland");

Comparator.Compare (p 1, p 2); // > 0
Comparator.Reversed (). Compare (p 1, p 2); // < 0

Optional

Optional 不是函数式接口,而是用于防止 NullPointerException 的漂亮工具。这是下一节的一个重要概念,让我们快速了解一下 Optional 的工作原理。

Optional 是一个简单的容器,其值可能是 null 或者不是 null。在 Java 8 之前一般某个函数应该返回非空对象但是有时却什么也没有返回,而在 Java 8 中,你应该返回 Optional 而不是 null。

译者注:示例中每个方法的作用已经添加。

1
2
3
4
5
6
7
8
9
10
//of ():为非 null 的值创建一个 Optional
Optional<String> optional = Optional.Of ("bam");
// isPresent ():如果值存在返回 true,否则返回 false
Optional.IsPresent (); // true
//get ():如果 Optional 有值则将其返回,否则抛出 NoSuchElementException
Optional.Get (); // "bam"
//orElse ():如果有值则将其返回,否则返回指定的其它值
Optional.OrElse ("fallback"); // "bam"
//ifPresent ():如果 Optional 实例有值则为其调用 consumer,否则不做处理
optional.IfPresent ((s) -> System.Out.Println (s.charAt (0))); // "b"

推荐阅读:[Java 8]如何正确使用 Optionalopen in new window

Streams (流)

java. Util. Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回 Stream 本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java. Util. Collection 的子类,List 或者 Set, Map 不支持。Stream 的操作可以串行执行或者并行执行。

首先看看 Stream 是怎么用,首先创建实例代码需要用到的数据 List:

1
2
3
4
5
6
7
8
9
List<String> stringList = new ArrayList<>();
StringList.Add ("ddd 2");
StringList.Add ("aaa 2");
StringList.Add ("bbb 1");
StringList.Add ("aaa 1");
StringList.Add ("bbb 3");
StringList.Add ("ccc");
StringList.Add ("bbb 2");
StringList.Add ("ddd 1");

Java 8 扩展了集合类,可以通过 Collection. Stream () 或者 Collection. ParallelStream () 来创建一个 Stream。下面几节将详细解释常用的 Stream 操作:

Filter (过滤)

过滤通过一个 predicate 接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他 Stream 操作(比如 forEach)。ForEach 需要一个函数来对过滤后的元素依次执行。ForEach 是一个最终操作,所以我们不能在 forEach 之后来执行其他 Stream 操作。

1
2
3
4
5
// 测试 Filter (过滤)
StringList
.stream ()
.filter ((s) -> s.startsWith ("a"))
.forEach (System. Out::println);//aaa 2 aaa 1

ForEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。

Sorted (排序)

排序是一个 中间操作,返回的是排序好后的 Stream。如果你不指定一个自定义的 Comparator 则会使用默认排序。

1
2
3
4
5
6
// 测试 Sort (排序)
StringList
.stream ()
.sorted ()
.filter ((s) -> s.startsWith ("a"))
.forEach (System. Out::println);// aaa 1 aaa 2

需要注意的是,排序只创建了一个排列好后的 Stream,而不会影响原有的数据源,排序之后原数据 stringList 是不会被修改的:

1
System.Out.Println (stringList);// ddd 2, aaa 2, bbb 1, aaa 1, bbb 3, ccc, bbb 2, ddd 1

Map (映射)

中间操作 map 会将元素根据指定的 Function 接口来依次将元素转成另外的对象。

下面的示例展示了将字符串转换为大写字符串。你也可以通过 map 来将对象转换成其他类型,map 返回的 Stream 类型是根据你 map 传递进去的函数的返回值决定的。

1
2
3
4
5
6
// 测试 Map 操作
StringList
.stream ()
.map (String::toUpperCase)
.sorted ((a, b) -> b.compareTo (a))
.forEach (System. Out::println);// "DDD 2", "DDD 1", "CCC", "BBB 3", "BBB 2", "BBB 1", "AAA 2", "AAA 1"

Match (匹配)

Stream 提供了多种匹配操作,允许检测指定的 Predicate 是否匹配整个 Stream。所有的匹配操作都是 最终操作 ,并返回一个 boolean 类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 测试 Match (匹配)操作
Boolean anyStartsWithA =
StringList
.stream ()
.anyMatch ((s) -> s.startsWith ("a"));
System.Out.Println (anyStartsWithA); // true

Boolean allStartsWithA =
StringList
.stream ()
.allMatch ((s) -> s.startsWith ("a"));

System.Out.Println (allStartsWithA); // false

Boolean noneStartsWithZ =
StringList
.stream ()
.noneMatch ((s) -> s.startsWith ("z"));

System.Out.Println (noneStartsWithZ); // true

Count (计数)

计数是一个 最终操作,返回 Stream 中元素的个数,返回值类型是 long

1
2
3
4
5
6
7
//测试 Count (计数)操作
Long startsWithB =
StringList
.stream ()
.filter ((s) -> s.startsWith ("b"))
.count ();
System.Out.Println (startsWithB); // 3

Reduce (规约)

这是一个 最终操作 ,允许通过指定的函数来将 stream 中的多个元素规约为一个元素,规约后的结果是通过 Optional 接口表示的:

1
2
3
4
5
6
7
8
//测试 Reduce (规约)操作
Optional<String> reduced =
StringList
.stream ()
.sorted ()
.reduce ((s 1, s 2) -> s 1 + "#" + s 2);

reduced.IfPresent (System. Out::println);//aaa 1 #aaa2 #bbb1 #bbb2 #bbb3 #ccc #ddd1 #ddd2

译者注: 这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于 Integer sum = integers.Reduce (0, (a, b) -> a+b); 也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。

1
2
3
4
5
6
7
8
9
10
11
12
// 字符串连接,concat = "ABCD"
String concat = Stream.Of ("A", "B", "C", "D"). Reduce ("", String::concat);
// 求最小值,minValue = -3.0
Double minValue = Stream.Of (-1.5, 1.0, -3.0, -2.0). Reduce (Double. MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
Int sumValue = Stream.Of (1, 2, 3, 4). Reduce (0, Integer::sum);
// 求和,sumValue = 10, 无起始值
SumValue = Stream.Of (1, 2, 3, 4). Reduce (Integer::sum). Get ();
// 过滤,字符串连接,concat = "ace"
Concat = Stream.Of ("a", "B", "c", "D", "e", "F").
filter (x -> x.compareTo ("Z") > 0).
Reduce ("", String::concat);

上面代码例如第一个示例的 reduce (),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce () 都返回具体的对象。而对于第四个示例没有起始值的 reduce (),由于可能没有足够的元素,返回的是 Optional,请留意这个区别。更多内容查看:IBM:Java 8 中的 Streams API 详解open in new window

Parallel Streams (并行流)

前面提到过 Stream 有串行和并行两种,串行 Stream 上的操作是在一个线程中依次完成,而并行 Stream 则是在多个线程上同时执行。

下面的例子展示了是如何通过并行 Stream 来提升性能:

首先我们创建一个没有重复元素的大表:

1
2
3
4
5
6
Int max = 1000000;
List<String> values = new ArrayList<>(max);
For (int i = 0; i < max; i++) {
UUID uuid = UUID.RandomUUID ();
Values.Add (uuid.ToString ());
}

我们分别用串行和并行两种方式对其进行排序,最后看看所用时间的对比。

Sequential Sort (串行排序)

1
2
3
4
5
6
7
8
9
//串行排序
Long t 0 = System.NanoTime ();
Long count = values.Stream (). Sorted (). Count ();
System.Out.Println (count);

Long t 1 = System.NanoTime ();

Long millis = TimeUnit.NANOSECONDS.ToMillis (t 1 - t 0);
System.Out.Println (String.Format ("sequential sort took: %d ms", millis));
1
2
1000000
Sequential sort took: 709 ms//串行排序所用的时间

Parallel Sort (并行排序)

1
2
3
4
5
6
7
8
9
10
11
//并行排序
Long t 0 = System.NanoTime ();

Long count = values.ParallelStream (). Sorted (). Count ();
System.Out.Println (count);

Long t 1 = System.NanoTime ();

Long millis = TimeUnit.NANOSECONDS.ToMillis (t 1 - t 0);
System.Out.Println (String.Format ("parallel sort took: %d ms", millis));

1
2
1000000
Parallel sort took: 475 ms//串行排序所用的时间

上面两个代码几乎是一样的,但是并行版的快了 50% 左右,唯一需要做的改动就是将 stream () 改为 parallelStream ()

Maps

前面提到过,Map 类型不支持 streams,不过 Map 提供了一些新的有用的方法来处理一些日常任务。Map 接口本身没有可用的 stream () 方法,但是你可以在键,值上创建专门的流或者通过 map.KeySet (). Stream (), map.Values (). Stream ()map.EntrySet (). Stream ()

此外, Maps 支持各种新的和有用的方法来执行常见任务。

1
2
3
4
5
6
7
Map<Integer, String> map = new HashMap<>();

For (int i = 0; i < 10; i++) {
Map.PutIfAbsent (i, "val" + i);
}

Map.ForEach ((id, val) -> System.Out.Println (val));//val 0 val 1 val 2 val 3 val 4 val 5 val 6 val 7 val 8 val 9

putIfAbsent 阻止我们在 null 检查时写入额外的代码; forEach 接受一个 consumer 来对 map 中的每个元素操作。

此示例显示如何使用函数在 map 上计算代码:

1
2
3
4
5
6
7
8
9
10
11
Map.ComputeIfPresent (3, (num, val) -> val + num);
Map.Get (3); // val 33

Map.ComputeIfPresent (9, (num, val) -> null);
Map.ContainsKey (9); // false

Map.ComputeIfAbsent (23, num -> "val" + num);
Map.ContainsKey (23); // true

Map.ComputeIfAbsent (3, num -> "bam");
Map.Get (3); // val 33

接下来展示如何在 Map 里删除一个键值全都匹配的项:

1
2
3
4
Map.Remove (3, "val 3");
Map.Get (3); // val 33
Map.Remove (3, "val 33");
Map.Get (3); // null

另外一个有用的方法:

1
Map.GetOrDefault (42, "not found");  // not found

对 Map 的元素做合并也变得很容易了:

1
2
3
4
Map.Merge (9, "val 9", (value, newValue) -> value.Concat (newValue));
Map.Get (9); // val 9
Map.Merge (9, "concat", (value, newValue) -> value.Concat (newValue));
Map.Get (9); // val 9 concat

Merge 做的事情是如果键名不存在则插入,否则对原键对应的值做合并操作并重新插入到 map 中。

Date API (日期相关 API)

Java 8 在 java. Time 包下包含一个全新的日期和时间 API。新的 Date API 与 Joda-Time 库相似,但它们不一样。以下示例涵盖了此新 API 的最重要部分。译者对这部分内容参考相关书籍做了大部分修改。

译者注 (总结):

  • Clock 类提供了访问当前日期和时间的方法,Clock 是时区敏感的,可以用来取代 System.CurrentTimeMillis () 来获取当前的微秒数。某一个特定的时间点也可以使用 Instant 类来表示,Instant 类也可以用来创建旧版本的 java. Util. Date 对象。

  • 在新 API 中时区使用 ZoneId 来表示。时区可以很方便的使用静态方法 of 来获取到。抽象类 ZoneId(在 java. Time 包中)表示一个区域标识符。它有一个名为 getAvailableZoneIds 的静态方法,它返回所有区域标识符。

  • jdk 1.8 中新增了 LocalDate 与 LocalDateTime 等类来解决日期处理方法,同时引入了一个新的类 DateTimeFormatter 来解决日期格式化问题。可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。

Clock

Clock 类提供了访问当前日期和时间的方法,Clock 是时区敏感的,可以用来取代 System.CurrentTimeMillis () 来获取当前的微秒数。某一个特定的时间点也可以使用 Instant 类来表示,Instant 类也可以用来创建旧版本的 java. Util. Date 对象。

1
2
3
4
5
6
7
Clock clock = Clock.SystemDefaultZone ();
Long millis = clock.Millis ();
System.Out.Println (millis);//1552379579043
Instant instant = clock.Instant ();
System.Out.Println (instant);
Date legacyDate = Date.From (instant); //2019-03-12 T08:46:42.588 Z
System.Out.Println (legacyDate);//Tue Mar 12 16:32:59 CST 2019

Timezones (时区)

在新 API 中时区使用 ZoneId 来表示。时区可以很方便的使用静态方法 of 来获取到。抽象类 ZoneId(在 java. Time 包中)表示一个区域标识符。它有一个名为 getAvailableZoneIds 的静态方法,它返回所有区域标识符。

1
2
3
4
5
6
7
//输出所有区域标识符
System.Out.Println (ZoneId.GetAvailableZoneIds ());

ZoneId zone 1 = ZoneId.Of ("Europe/Berlin");
ZoneId zone 2 = ZoneId.Of ("Brazil/East");
System.Out.Println (zone 1.GetRules ());// ZoneRules[currentStandardOffset=+01:00]
System.Out.Println (zone 2.GetRules ());// ZoneRules[currentStandardOffset=-03:00]

LocalTime (本地时间)

LocalTime 定义了一个没有时区信息的时间,例如晚上 10 点或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差:

1
2
3
4
5
6
7
8
9
LocalTime now 1 = LocalTime.Now (zone 1);
LocalTime now 2 = LocalTime.Now (zone 2);
System.Out.Println (now 1.IsBefore (now 2)); // false

Long hoursBetween = ChronoUnit.HOURS.Between (now 1, now 2);
Long minutesBetween = ChronoUnit.MINUTES.Between (now 1, now 2);

System.Out.Println (hoursBetween); // -3
System.Out.Println (minutesBetween); // -239

LocalTime 提供了多种工厂方法来简化对象的创建,包括解析时间字符串.

1
2
3
4
5
6
7
8
9
LocalTime late = LocalTime.Of (23, 59, 59);
System.Out.Println (late); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedTime (FormatStyle. SHORT)
.withLocale (Locale. GERMAN);

LocalTime leetTime = LocalTime.Parse ("13:37", germanFormatter);
System.Out.Println (leetTime); // 13:37

LocalDate (本地日期)

LocalDate 表示了一个确切的日期,比如 2014-03-11。该对象值是不可变的,用起来和 LocalTime 基本一致。下面的例子展示了如何给 Date 对象加减天/月/年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例。

1
2
3
4
5
6
7
8
9
LocalDate today = LocalDate.Now ();//获取现在的日期
System.Out.Println ("今天的日期: "+today);//2019-03-12
LocalDate tomorrow = today.Plus (1, ChronoUnit. DAYS);
System.Out.Println ("明天的日期: "+tomorrow);//2019-03-13
LocalDate yesterday = tomorrow.MinusDays (2);
System.Out.Println ("昨天的日期: "+yesterday);//2019-03-11
LocalDate independenceDay = LocalDate.Of (2019, Month. MARCH, 12);
DayOfWeek dayOfWeek = independenceDay.GetDayOfWeek ();
System.Out.Println ("今天是周几: "+dayOfWeek);//TUESDAY

从字符串解析一个 LocalDate 类型和解析 LocalTime 一样简单, 下面是使用 DateTimeFormatter 解析字符串的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String str 1 = "2014==04==12 01 时 06 分 09 秒";
// 根据需要解析的日期、时间字符串定义解析所用的格式器
DateTimeFormatter fomatter 1 = DateTimeFormatter
.ofPattern ("yyyy==MM==dd HH 时 mm 分 ss 秒");

LocalDateTime dt 1 = LocalDateTime.Parse (str 1, fomatter 1);
System.Out.Println (dt 1); // 输出 2014-04-12 T01:06:09

String str 2 = "2014$$$四月$$$13 20 小时";
DateTimeFormatter fomatter 2 = DateTimeFormatter
.ofPattern ("yyy$$$MMM$$$dd HH 小时");
LocalDateTime dt 2 = LocalDateTime.Parse (str 2, fomatter 2);
System.Out.Println (dt 2); // 输出 2014-04-13 T20:00

再来看一个使用 DateTimeFormatter 格式化日期的示例

1
2
3
4
5
LocalDateTime rightNow=LocalDateTime.Now ();
String date=DateTimeFormatter. ISO_LOCAL_DATE_TIME.Format (rightNow);
System.Out.Println (date);//2019-03-12 T16:26:48.29
DateTimeFormatter formatter=DateTimeFormatter.OfPattern ("YYYY-MM-dd HH:mm: ss");
System.Out.Println (formatter.Format (rightNow));//2019-03-12 16:26:48

🐛 修正(参见:issue#1157open in new window:使用 YYYY 显示年份时,会显示当前时间所在周的年份,在跨年周会有问题。一般情况下都使用 yyyy,来显示准确的年份。

跨年导致日期显示错误示例:

1
2
3
4
5
6
7
8
9
10
11
LocalDateTime rightNow = LocalDateTime.Of (2020, 12, 31, 12, 0, 0);
String date= DateTimeFormatter. ISO_LOCAL_DATE_TIME.Format (rightNow);
// 2020-12-31 T12:00:00
System.Out.Println (date);
DateTimeFormatter formatterOfYYYY = DateTimeFormatter.OfPattern ("YYYY-MM-dd HH:mm: ss");
// 2021-12-31 12:00:00
System.Out.Println (formatterOfYYYY.Format (rightNow));

DateTimeFormatter formatterOfYyyy = DateTimeFormatter.OfPattern ("yyyy-MM-dd HH:mm: ss");
// 2020-12-31 12:00:00
System.Out.Println (formatterOfYyyy.Format (rightNow));

从下图可以更清晰的看到具体的错误,并且 IDEA 已经智能地提示更倾向于使用 yyyy 而不是 YYYY

LocalDateTime (本地日期时间)

LocalDateTime 同时表示了时间和日期,相当于前两节内容合并到一个对象上了。LocalDateTime 和 LocalTime 还有 LocalDate 一样,都是不可变的。LocalDateTime 提供了一些能访问具体字段的方法。

1
2
3
4
5
6
7
8
9
10
LocalDateTime sylvester = LocalDateTime.Of (2014, Month. DECEMBER, 31, 23, 59, 59);

DayOfWeek dayOfWeek = sylvester.GetDayOfWeek ();
System.Out.Println (dayOfWeek); // WEDNESDAY

Month month = sylvester.GetMonth ();
System.Out.Println (month); // DECEMBER

Long minuteOfDay = sylvester.GetLong (ChronoField. MINUTE_OF_DAY);
System.Out.Println (minuteOfDay); // 1439

只要附加上时区信息,就可以将其转换为一个时间点 Instant 对象,Instant 时间点对象可以很容易的转换为老式的 java. Util. Date

1
2
3
4
5
6
Instant instant = sylvester
.atZone (ZoneId.SystemDefault ())
.toInstant ();

Date legacyDate = Date.From (instant);
System.Out.Println (legacyDate); // Wed Dec 31 23:59:59 CET 2014

格式化 LocalDateTime 和格式化时间和日期一样的,除了使用预定义好的格式外,我们也可以自己定义格式:

1
2
3
4
5
6
DateTimeFormatter formatter =
DateTimeFormatter
.ofPattern ("MMM dd, yyyy - HH: mm");
LocalDateTime parsed = LocalDateTime.Parse ("Nov 03, 2014 - 07:13", formatter);
String string = formatter.Format (parsed);
System.Out.Println (string); // Nov 03, 2014 - 07:13

和 java. Text. NumberFormat 不一样的是新版的 DateTimeFormatter 是不可变的,所以它是线程安全的。关于时间日期格式的详细信息在这里open in new window

Annotations (注解)

在 Java 8 中支持多重注解了,先看个例子来理解一下是什么意思。首先定义一个包装类 Hints 注解用来放置一组具体的 Hint 注解:

1
2
3
4
5
6
7
8
@Retention (RetentionPolicy. RUNTIME)
@interface Hints {
Hint[] value ();
}
@Repeatable (Hints. Class)
@interface Hint {
String value ();
}

Java 8 允许我们把同一个类型的注解使用多次,只需要给该注解标注一下 @Repeatable 即可。

例 1: 使用包装类当容器来存多个注解(老方法)

1
2
@Hints ({@Hint ("hint 1"), @Hint ("hint 2")})
Class Person {}

例 2:使用多重注解(新方法)

1
2
3
@Hint ("hint 1")
@Hint ("hint 2")
Class Person {}

第二个例子里 java 编译器会隐性的帮你定义好@Hints 注解,了解这一点有助于你用反射来获取这些信息:

1
2
3
4
5
6
7
Hint hint = Person.Class.GetAnnotation (Hint. Class);
System.Out.Println (hint); // null
Hints hints 1 = Person.Class.GetAnnotation (Hints. Class);
System.Out.Println (hints 1.Value (). Length); // 2

Hint[] hints 2 = Person.Class.GetAnnotationsByType (Hint. Class);
System.Out.Println (hints 2. Length); // 2

即便我们没有在 Person 类上定义 @Hints 注解,我们还是可以通过 getAnnotation (Hints. Class) 来获取 @Hints 注解,更加方便的方法是使用 getAnnotationsByType 可以直接获取到所有的 @Hint 注解。另外 Java 8 的注解还增加到两种新的 target 上了:

1
2
@Target ({ElementType. TYPE_PARAMETER, ElementType. TYPE_USE})
@interface MyAnnotation {}

Java 9 新特性概览

Java 9 发布于 2017 年 9 月 21 日。作为 Java 8 之后 3 年半才发布的新版本,Java 9 带来了很多重大的变化其中最重要的改动是 Java 平台模块系统的引入,其他还有诸如集合、Stream 流……。

你可以在 Archived OpenJDK General-Availability Releasesopen in new window 上下载自己需要的 JDK 版本!官方的新特性说明文档地址: https://openjdk.java.net/projects/jdk/

概览(精选了一部分)

JShell

JShell 是 Java 9 新增的一个实用工具。为 Java 提供了类似于 Python 的实时命令行交互工具。

在 JShell 中可以直接输入表达式并查看其执行结果。

JShell 为我们带来了哪些好处呢?

  1. 降低了输出第一行 Java 版”Hello World!”的门槛,能够提高新手的学习热情。
  2. 在处理简单的小逻辑,验证简单的小问题时,比 IDE 更有效率(并不是为了取代 IDE,对于复杂逻辑的验证,IDE 更合适,两者互补)。
  3. ……

JShell 的代码和普通的可编译代码,有什么不一样?

  1. 一旦语句输入完成,JShell 立即就能返回执行的结果,而不再需要编辑器、编译器、解释器。
  2. JShell 支持变量的重复声明,后面声明的会覆盖前面声明的。
  3. JShell 支持独立的表达式比如普通的加法运算 1 + 1
  4. ……

模块化系统

模块系统是 Jigsaw Projectopen in new window 的一部分,把模块化开发实践引入到了 Java 平台中,可以让我们的代码可重用性更好!

什么是模块系统? 官方的定义是:

A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor。

简单来说,你可以将一个模块看作是一组唯一命名、可重用的包、资源和模块描述文件(module-info. Java)。

任意一个 jar 文件,只要加上一个模块描述文件(module-info. Java),就可以升级为一个模块。

在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlinkopen in new window 工具 (Jlink 是随 Java 9 一起发布的新命令行工具。它允许开发人员为基于模块的 Java 应用程序创建自己的轻量级、定制的 JRE),创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。

我们可以通过 exports 关键词精准控制哪些类可以对外开放使用,哪些类只能内部使用。

1
2
3
4
5
6
7
8
9
Module my. Module {
//exports 公开指定包的所有公共成员
Exports com. My. Package. Name;
}

Module my. Module {
//exports…to 限制访问的成员范围
Export com. My. Package. Name to com. Specific. Package;
}

想要深入了解 Java 9 的模块化,可以参考下面这几篇文章:

G 1 成为默认垃圾回收器

在 Java 8 的时候,默认垃圾回收器是 Parallel Scavenge(新生代)+Parallel Old(老年代)。到了 Java 9, CMS 垃圾回收器被废弃了,G 1(Garbage-First Garbage Collector) 成为了默认垃圾回收器。

G 1 还是在 Java 7 中被引入的,经过两个版本优异的表现成为成为默认垃圾回收器。

快速创建不可变集合

增加了 List.Of ()Set.Of ()Map.Of ()Map.OfEntries () 等工厂方法来创建不可变集合(有点参考 Guava 的味道):

1
2
3
List.Of ("Java", "C++");
Set.Of ("Java", "C++");
Map.Of ("Java", 1, "C++", 2);

使用 of () 创建的集合为不可变集合,不能进行添加、删除、替换、排序等操作,不然会报 java. Lang. UnsupportedOperationException 异常。

String 存储结构优化

Java 8 及之前的版本,String 一直是用 char[] 存储。在 Java 9 之后,String 的实现改用 byte[] 数组存储字符串,节省了空间。

1
2
3
4
5
public final class String implements java. Io. Serializable, Comparable<String>, CharSequence {
// @Stable 注解表示变量最多被修改一次,称为“稳定的”。
@Stable
Private final byte[] value;
}

接口私有方法

Java 9 允许在接口中使用私有方法。这样的话,接口的使用就更加灵活了,有点像是一个简化版的抽象类。

1
2
3
4
Public interface MyInterface {
Private void methodPrivate (){
}
}

Try-with-resources 增强

在 Java 9 之前,我们只能在 try-with-resources 块中声明变量:

1
2
3
4
Try (Scanner scanner = new Scanner (new File ("testRead. Txt"));
PrintWriter writer = new PrintWriter (new File ("testWrite. Txt"))) {
// omitted
}

在 Java 9 之后,在 try-with-resources 语句中可以使用 effectively-final 变量。

1
2
3
4
5
Final Scanner scanner = new Scanner (new File ("testRead. Txt"));
PrintWriter writer = new PrintWriter (new File ("testWrite. Txt"))
Try (scanner; writer) {
// omitted
}

什么是 effectively-final 变量? 简单来说就是没有被 final 修饰但是值在初始化后从未更改的变量。

正如上面的代码所演示的那样,即使 writer 变量没有被显示声明为 final,但它在第一次被复制后就不会改变了,因此,它就是 effectively-final 变量。

Stream & Optional 增强

Stream 中增加了新的方法 ofNullable ()dropWhile ()takeWhile () 以及 iterate () 方法的重载方法。

Java 9 中的 ofNullable () 方法允许我们创建一个单元素的 Stream,可以包含一个非空元素,也可以创建一个空 Stream。而在 Java 8 中则不可以创建空的 Stream

1
2
3
4
Stream<String> stringStream = Stream.OfNullable ("Java");
System.Out.Println (stringStream.Count ());// 1
Stream<String> nullStream = Stream.OfNullable (null);
System.Out.Println (nullStream.Count ());//0

takeWhile () 方法可以从 Stream 中依次获取满足条件的元素,直到不满足条件为止结束获取。

1
2
List<Integer> integerList = List.Of (11, 33, 66, 8, 9, 13);
IntegerList.Stream (). TakeWhile (x -> x < 50). ForEach (System. Out::println);// 11 33

dropWhile () 方法的效果和 takeWhile () 相反。

1
2
List<Integer> integerList 2 = List.Of (11, 33, 66, 8, 9, 13);
IntegerList 2.Stream (). DropWhile (x -> x < 50). ForEach (System. Out::println);// 66 8 9 13

iterate () 方法的新重载方法提供了一个 Predicate 参数 (判断条件)来决定什么时候结束迭代

1
2
3
4
5
6
public static<T> Stream<T> iterate (final T seed, final UnaryOperator<T> f) {
}
// 新增加的重载方法
public static<T> Stream<T> iterate (T seed, Predicate<? super T> hasNext, UnaryOperator<T> next) {

}

两者的使用对比如下,新的 iterate () 重载方法更加灵活一些。

1
2
3
4
// 使用原始 iterate () 方法输出数字 1~10
Stream.Iterate (1, i -> i + 1). Limit (10). ForEach (System. Out::println);
// 使用新的 iterate () 重载方法输出数字 1~10
Stream.Iterate (1, i -> i <= 10, i -> i + 1). ForEach (System. Out::println);

Optional 类中新增了 ifPresentOrElse ()or ()stream () 等方法

ifPresentOrElse () 方法接受两个参数 ConsumerRunnable ,如果 Optional 不为空调用 Consumer 参数,为空则调用 Runnable 参数。

1
2
3
4
public void ifPresentOrElse (Consumer<? super T> action, Runnable emptyAction)

Optional<Object> objectOptional = Optional.Empty ();
ObjectOptional.IfPresentOrElse (System. Out:: println, () -> System.Out.Println ("Empty!!!"));// Empty!!!

or () 方法接受一个 Supplier 参数,如果 Optional 为空则返回 Supplier 参数指定的 Optional 值。

1
2
3
4
public Optional<T> or (Supplier<? extends Optional<? extends T>> supplier)

Optional<Object> objectOptional = Optional.Empty ();
ObjectOptional.Or (() -> Optional.Of ("java")). IfPresent (System. Out::println);//java

进程 API

Java 9 增加了 java. Lang. ProcessHandle 接口来实现对原生进程进行管理,尤其适合于管理长时间运行的进程。

1
2
3
4
5
6
// 获取当前正在运行的 JVM 的进程
ProcessHandle currentProcess = ProcessHandle.Current ();
// 输出进程的 id
System.Out.Println (currentProcess.Pid ());
// 输出进程的信息
System.Out.Println (currentProcess.Info ());

ProcessHandle 接口概览:

响应式流 ( Reactive Streams )

在 Java 9 中的 java. Util. Concurrent. Flow 类中新增了反应式流规范的核心接口。

Flow 中包含了 Flow. PublisherFlow. SubscriberFlow. SubscriptionFlow. Processor 等 4 个核心接口。Java 9 还提供了 SubmissionPublisher 作为 Flow. Publisher 的一个实现。

关于 Java 9 响应式流更详细的解读,推荐你看 Java 9 揭秘(17. Reactive Streams )- 林本托open in new window 这篇文章。

变量句柄

变量句柄是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等。

变量句柄的含义类似于已有的方法句柄 MethodHandle ,由 Java 类 java. Lang. Invoke. VarHandle 来表示,可以使用类 java. Lang. Invoke. MethodHandles. Lookup 中的静态工厂方法来创建 VarHandle 对象。

VarHandle 的出现替代了 java. Util. Concurrent. Atomicsun. Misc. Unsafe 的部分操作。并且提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的 API。

其它

  • 平台日志 API 改进:Java 9 允许为 JDK 和应用配置同样的日志实现。新增了 System. LoggerFinder 用来管理 JDK 使用的日志记录器实现。JVM 在运行时只有一个系统范围的 LoggerFinder 实例。我们可以通过添加自己的 System. LoggerFinder 实现来让 JDK 和应用使用 SLF 4 J 等其他日志记录框架。
  • CompletableFuture 类增强:新增了几个新的方法(completeAsyncorTimeout 等)。
  • Nashorn 引擎的增强:Nashorn 是从 Java 8 开始引入的 JavaScript 引擎,Java 9 对 Nashorn 做了些增强,实现了一些 ES 6 的新特性(Java 11 中已经被弃用)。
  • I/O 流的新特性:增加了新的方法来读取和复制 InputStream 中包含的数据。
  • 改进应用的安全性能:Java 9 新增了 4 个 SHA- 3 哈希算法,SHA 3-224、SHA 3-256、SHA 3-384 和 SHA 3-512。
  • 改进方法句柄(Method Handle):方法句柄从 Java 7 开始引入,Java 9 在类 java. Lang. Invoke. MethodHandles 中新增了更多的静态方法来创建不同类型的方法句柄。

Java 10 新特性概览

Java 10 发布于 2018 年 3 月 20 日,最知名的特性应该是 var 关键字(局部变量类型推断)的引入了,其他还有垃圾收集器改善、GC 改进、性能提升、线程管控等一批新特性。

概览(精选了一部分)

局部变量类型推断 (var)

由于太多 Java 开发者希望 Java 中引入局部变量推断,于是 Java 10 的时候它来了,也算是众望所归了!

Java 10 提供了 var 关键字声明局部变量。

1
2
3
4
5
6
7
8
9
Var id = 0;
var codefx = new URL (" https://mp.weixin.qq.com/" );
var list = new ArrayList<>();
Var list = List.Of (1, 2, 3);
var map = new HashMap<String, String>();
Var p = Paths.Of ("src/test/java/Java 9 FeaturesTest. Java");
Var numbers = List.Of ("a", "b", "c");
For (var n : list)
System.Out.Print (n+ " ");

Var 关键字只能用于带有构造器的局部变量和 for 循环中。

1
2
3
Var count=null; //❌编译不通过,不能声明为 null
Var r = () -> Math.Random ();//❌编译不通过, 不能声明为 Lambda 表达式
Var array = {1,2,3};//❌编译不通过, 不能声明数组

Var 并不会改变 Java 是一门静态类型语言的事实,编译器负责推断出类型。

另外,Scala 和 Kotlin 中已经有了 val 关键字 ( final var 组合关键字)。

相关阅读:《Java 10 新特性之局部变量类型推断》open in new window

垃圾回收器接口

在早期的 JDK 结构中,组成垃圾收集器 (GC) 实现的组件分散在代码库的各个部分。 Java 10 通过引入一套纯净的垃圾收集器接口来将不同垃圾收集器的源代码分隔开。

G 1 并行 Full GC

从 Java 9 开始 G 1 就了默认的垃圾回收器,G 1 是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC, 但是 Java 9 的 G 1 的 FullGC 依然是使用单线程去完成标记清除算法, 这可能会导致垃圾回收期在无法回收内存的时候触发 Full GC。

为了最大限度地减少 Full GC 造成的应用停顿的影响,从 Java 10 开始,G 1 的 FullGC 改为并行的标记清除算法,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量。

集合增强

ListSetMap 提供了静态方法 copyOf () 返回入参集合的一个不可变拷贝。

1
2
3
static <E> List<E> copyOf (Collection<? extends E> coll) {
Return ImmutableCollections.ListCopy (coll);
}

使用 copyOf () 创建的集合为不可变集合,不能进行添加、删除、替换、排序等操作,不然会报 java. Lang. UnsupportedOperationException 异常。 IDEA 也会有相应的提示。

并且,java. Util. Stream. Collectors 中新增了静态方法,用于将流中的元素收集为不可变的集合。

1
2
3
var list = new ArrayList<>();
List.Stream (). Collect (Collectors.ToUnmodifiableList ());
List.Stream (). Collect (Collectors.ToUnmodifiableSet ());

Optional 增强

Optional 新增了 orElseThrow () 方法来在没有值时抛出指定的异常。

1
2
Optional.OfNullable (cache.GetIfPresent (key))
.orElseThrow (() -> new PrestoException (NOT_FOUND, "Missing entry found for key: " + key));

应用程序类数据共享 (扩展 CDS 功能)

在 Java 5 中就已经引入了类数据共享机制 (Class Data Sharing,简称 CDS),允许将一组类预处理为共享归档文件,以便在运行时能够进行内存映射以减少 Java 程序的启动时间,当多个 Java 虚拟机(JVM)共享相同的归档文件时,还可以减少动态内存的占用量,同时减少多个虚拟机在同一个物理或虚拟的机器上运行时的资源占用。CDS 在当时还是 Oracle JDK 的商业特性。

Java 10 在现有的 CDS 功能基础上再次拓展,以允许应用类放置在共享存档中。CDS 特性在原来的 bootstrap 类基础之上,扩展加入了应用类的 CDS 为 (Application Class-Data Sharing,AppCDS) 支持,大大加大了 CDS 的适用范围。其原理为:在启动时记录加载类的过程,写入到文本文件中,再次启动时直接读取此启动文本并加载。设想如果应用环境没有大的变化,启动速度就会得到提升。

实验性的基于 Java 的 JIT 编译器

Graal 是一个基于 Java 语言编写的 JIT 编译器,是 JDK 9 中引入的实验性 Ahead-of-Time (AOT) 编译器的基础。

Oracle 的 HotSpot VM 便附带两个用 C++ 实现的 JIT compiler:C 1 及 C 2。在 Java 10 (Linux/x 64, macOS/x 64) 中,默认情况下 HotSpot 仍使用 C 2,但通过向 java 命令添加 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 参数便可将 C 2 替换成 Graal。

相关阅读:深入浅出 Java 10 的实验性 JIT 编译器 Graal - 郑雨迪open in new window

其他

  • 线程-局部管控:Java 10 中线程管控引入 JVM 安全点的概念,将允许在不运行全局 JVM 安全点的情况下实现线程回调,由线程本身或者 JVM 线程来执行,同时保持线程处于阻塞状态,这种方式使得停止单个线程变成可能,而不是只能启用或停止所有线程
  • 备用存储装置上的堆分配:Java 10 中将使得 JVM 能够使用适用于不同类型的存储机制的堆,在可选内存设备上进行堆内存分配
  • ……

Java 11 新特性概览

Java 11 于 2018 年 9 月 25 日正式发布,这是很重要的一个版本!Java 11 和 2017 年 9 月份发布的 Java 9 以及 2018 年 3 月份发布的 Java 10 相比,其最大的区别就是:在长期支持 (Long-Term-Support)方面,Oracle 表示会对 Java 11 提供大力支持,这一支持将会持续至 2026 年 9 月。这是据 Java 8 以后支持的首个长期版本。

下面这张图是 Oracle 官方给出的 Oracle JDK 支持的时间线。

概览(精选了一部分)

HTTP Client 标准化

Java 11 对 Java 9 中引入并在 Java 10 中进行了更新的 Http Client API 进行了标准化,在前两个版本中进行孵化的同时,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。

并且,Java 11 中,Http Client 的包名由 jdk. Incubator. Http 改为 java. Net. Http,该 API 通过 CompleteableFuture 提供非阻塞请求和响应语义。使用起来也很简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Var request = HttpRequest.NewBuilder ()
.uri (URI.Create (" https://javastack.cn" ))
.GET ()
.build ();
Var client = HttpClient.NewHttpClient ();

// 同步
HttpResponse<String> response = client.Send (request, HttpResponse.BodyHandlers.OfString ());
System.Out.Println (response.Body ());

// 异步
Client.SendAsync (request, HttpResponse.BodyHandlers.OfString ())
.thenApply (HttpResponse::body)
.thenAccept (System. Out::println);

String 增强

Java 11 增加了一系列的字符串处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//判断字符串是否为空
" ".IsBlank ();//true
//去除字符串首尾空格
" Java ".Strip ();// "Java"
//去除字符串首部空格
" Java ".StripLeading (); // "Java "
//去除字符串尾部空格
" Java ".StripTrailing (); // " Java"
//重复字符串多少次
"Java".Repeat (3); // "JavaJavaJava"
//返回由行终止符分隔的字符串集合。
"A\nB\nC".Lines (). Count (); // 3
"A\nB\nC".Lines (). Collect (Collectors.ToList ());

Optional 增强

新增了 isEmpty () 方法来判断指定的 Optional 对象是否为空。

1
2
Var op = Optional.Empty ();
System.Out.Println (op.IsEmpty ());//判断指定的 Optional 对象是否为空

ZGC (可伸缩低延迟垃圾收集器)

ZGC 即 Z Garbage Collector,是一个可伸缩的、低延迟的垃圾收集器。

ZGC 主要为了满足如下目标进行设计:

  • GC 停顿时间不超过 10 ms
  • 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆
  • 应用吞吐能力不会下降超过 15%(与 G 1 回收算法相比)
  • 方便在此基础上引入新的 GC 特性和利用 colored 针以及 Load barriers 优化奠定基础
  • 当前只支持 Linux/x 64 位平台

ZGC 目前 处在实验阶段,只支持 Linux/x 64 平台。

与 CMS 中的 ParNew 和 G 1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。

在 ZGC 中出现 Stop The World 的情况会更少!

详情可以看:《新一代垃圾回收器 ZGC 的探索与实践》open in new window

Lambda 参数的局部变量语法

从 Java 10 开始,便引入了局部变量类型推断这一关键特性。类型推断允许使用关键字 var 作为局部变量的类型而不是实际类型,编译器根据分配给变量的值推断出类型。

Java 10 中对 var 关键字存在几个限制

  • 只能用于局部变量上
  • 声明时必须初始化
  • 不能用作方法参数
  • 不能在 Lambda 表达式中使用

Java 11 开始允许开发者在 Lambda 表达式中使用 var 进行参数声明。

1
2
3
// 下面两者是等价的
Consumer<String> consumer = (var i) -> System.Out.Println (i);
Consumer<String> consumer = (String i) -> System.Out.Println (i);

启动单文件源代码程序

这意味着我们可以运行单一文件的 Java 源代码。此功能允许使用 Java 解释器直接执行 Java 源代码。源代码在内存中编译,然后由解释器执行,不需要在磁盘上生成 . Class 文件了。唯一的约束在于所有相关的类必须定义在同一个 Java 文件中。

对于 Java 初学者并希望尝试简单程序的人特别有用,并且能和 jshell 一起使用。一定能程度上增强了使用 Java 来写脚本程序的能力。

其他新特性

  • 新的垃圾回收器 Epsilon:一个完全消极的 GC 实现,分配有限的内存资源,最大限度的降低内存占用和内存吞吐延迟时间
  • 低开销的 Heap Profiling:Java 11 中提供一种低开销的 Java 堆分配采样方法,能够得到堆分配的 Java 对象信息,并且能够通过 JVMTI 访问堆信息
  • TLS 1.3 协议:Java 11 中包含了传输层安全性(TLS)1.3 规范(RFC 8446)的实现,替换了之前版本中包含的 TLS,包括 TLS 1.2,同时还改进了其他 TLS 功能,例如 OCSP 装订扩展(RFC 6066,RFC 6961),以及会话散列和扩展主密钥扩展(RFC 7627),在安全性和性能方面也做了很多提升
  • **飞行记录器 (Java Flight Recorder)**:飞行记录器之前是商业版 JDK 的一项分析工具,但在 Java 11 中,其代码被包含到公开代码库中,这样所有人都能使用该功能了。
  • ……

Java 12 & 13 新特性概览

Java 12

String 增强

Java 12 增加了两个的字符串处理方法,如以下所示。

indent () 方法可以实现字符串缩进。

1
2
3
4
5
6
String text = "Java";
// 缩进 4 格
Text = text.Indent (4);
System.Out.Println (text);
Text = text.Indent (-10);
System.Out.Println (text);

输出:

transform () 方法可以用来转变指定字符串。

1
2
String result = "foo".Transform (input -> input + " bar");
System.Out.Println (result); // foo bar

Files 增强(文件比较)

Java 12 添加了以下方法来比较两个文件:

1
Public static long mismatch (Path path, Path path 2) throws IOException

mismatch () 方法用于比较两个文件,并返回第一个不匹配字符的位置,如果文件相同则返回 -1 L。

代码示例(两个文件内容相同的情况):

1
2
3
4
5
6
7
Path filePath 1 = Files.CreateTempFile ("file 1", ". Txt");
Path filePath 2 = Files.CreateTempFile ("file 2", ". Txt");
Files.WriteString (filePath 1, "Java 12 Article");
Files.WriteString (filePath 2, "Java 12 Article");

Long mismatch = Files.Mismatch (filePath 1, filePath 2);
AssertEquals (-1, mismatch);

代码示例(两个文件内容不相同的情况):

1
2
3
4
5
6
7
Path filePath 3 = Files.CreateTempFile ("file 3", ". Txt");
Path filePath 4 = Files.CreateTempFile ("file 4", ". Txt");
Files.WriteString (filePath 3, "Java 12 Article");
Files.WriteString (filePath 4, "Java 12 Tutorial");

Long mismatch = Files.Mismatch (filePath 3, filePath 4);
AssertEquals (8, mismatch);

数字格式化工具类

NumberFormat 新增了对复杂的数字进行格式化的支持

1
2
3
NumberFormat fmt = NumberFormat.GetCompactNumberInstance (Locale. US, NumberFormat. Style. SHORT);
String result = fmt.Format (1000);
System.Out.Println (result);

输出:

Shenandoah GC

Redhat 主导开发的 Pauseless GC 实现,主要目标是 99.9% 的暂停小于 10 ms,暂停与堆大小无关等

和 Java 11 开源的 ZGC 相比(需要升级到 JDK 11 才能使用),Shenandoah GC 有稳定的 JDK 8 u 版本,在 Java 8 占据主要市场份额的今天有更大的可落地性。

G 1 收集器优化

Java 12 为默认的垃圾收集器 G 1 带来了两项更新:

  • 可中止的混合收集集合:JEP 344 的实现,为了达到用户提供的停顿时间目标,JEP 344 通过把要被回收的区域集(混合收集集合)拆分为强制和可选部分,使 G 1 垃圾回收器能中止垃圾回收过程。 G 1 可以中止可选部分的回收以达到停顿时间目标
  • 及时返回未使用的已分配内存:JEP 346 的实现,增强 G 1 GC,以便在空闲时自动将 Java 堆内存返回给操作系统

预览新特性

作为预览特性加入,需要在 javac 编译和 java 运行时增加参数 --enable-preview

增强 Switch

传统的 switch 语法存在容易漏写 break 的问题,而且从代码整洁性层面来看,多个 break 本质也是一种重复。

Java 12 增强了 switch 表达式,使用类似 lambda 语法条件匹配成功后的执行块,不需要多写 break 。

1
2
3
4
5
6
Switch (day) {
Case MONDAY, FRIDAY, SUNDAY -> System.Out.Println (6);
Case TUESDAY -> System.Out.Println (7);
Case THURSDAY, SATURDAY -> System.Out.Println (8);
Case WEDNESDAY -> System.Out.Println (9);
}
Instanceof 模式匹配

instanceof 主要在类型强转前探测对象的具体类型。

之前的版本中,我们需要显示地对对象进行类型转换。

1
2
3
4
5
Object obj = "我是字符串";
If (obj instanceof String){
String str = (String) obj;
System.Out.Println (str);
}

新版的 instanceof 可以在判断是否属于具体的类型同时完成转换。

1
2
3
4
Object obj = "我是字符串";
If (obj instanceof String str){
System.Out.Println (str);
}

Java 13

增强 ZGC (释放未使用内存)

在 Java 11 中实验性引入的 ZGC 在实际的使用中存在未能主动将未使用的内存释放给操作系统的问题。

ZGC 堆由一组称为 ZPages 的堆区域组成。在 GC 周期中清空 ZPages 区域时,它们将被释放并返回到页面缓存 ZPageCache 中,此缓存中的 ZPages 按最近最少使用(LRU)的顺序,并按照大小进行组织。

在 Java 13 中,ZGC 将向操作系统返回被标识为长时间未使用的页面,这样它们将可以被其他进程重用。

SocketAPI 重构

Java Socket API 终于迎来了重大更新!

Java 13 将 Socket API 的底层进行了重写, NioSocketImpl 是对 PlainSocketImpl 的直接替代,它使用 java. Util. Concurrent 包下的锁而不是同步方法。如果要使用旧实现,请使用 -Djdk. Net. UsePlainSocketImpl=true

并且,在 Java 13 中是默认使用新的 Socket 实现。

1
2
Public final class NioSocketImpl extends SocketImpl implements PlatformSocketImpl {
}

FileSystems

FileSystems 类中添加了以下三种新方法,以便更容易地使用将文件内容视为文件系统的文件系统提供程序:

  • newFileSystem (Path)
  • newFileSystem (Path, Map<String, ?>)
  • newFileSystem (Path, Map<String, ?>, ClassLoader)

动态 CDS 存档

Java 13 中对 Java 10 中引入的应用程序类数据共享 (AppCDS)进行了进一步的简化、改进和扩展,即:允许在 Java 应用程序执行结束时动态进行类归档,具体能够被归档的类包括所有已被加载,但不属于默认基层 CDS 的应用程序类和引用类库中的类。

这提高了应用程序类数据共享(AppCDSopen in new window)的可用性。无需用户进行试运行来为每个应用程序创建类列表。

1
2
$ java -XX:ArchiveClassesAtExit=my_app_cds. Jsa -cp my_app. Jar
$ java -XX:SharedArchiveFile=my_app_cds. Jsa -cp my_app. Jar

预览新特性

文本块

解决 Java 定义多行字符串时只能通过换行转义或者换行连接符来变通支持的问题,引入三重双引号来定义多行文本。

Java 13 支持两个 """ 符号中间的任何内容都会被解释为字符串的一部分,包括换行符。

未支持文本块之前的 HTML 写法:

1
2
3
4
String json ="{\n" +
" \"name\":\"mkyong\",\n" +
" \"age\": 38\n" +
"}\n";

支持文本块之后的 HTML 写法:

1
2
3
4
5
6
String json = """
{
"name": "mkyong",
"age":38
}
""";

未支持文本块之前的 SQL 写法:

1
2
3
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
"WHERE `CITY` = 'INDIANAPOLIS'\n" +
"ORDER BY `EMP_ID`, `LAST_NAME`;\n";

支持文本块之后的 SQL 写法:

1
2
3
4
5
String query = """
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = 'INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;
""";

另外,String 类新增加了 3 个新的方法来操作文本块:

  • formatted (Object... Args):它类似于 Stringformat () 方法。添加它是为了支持文本块的格式设置。
  • stripIndent ():用于去除文本块中每一行开头和结尾的空格。
  • translateEscapes ():转义序列如 “\\t” 转换为 “\t”

由于文本块是一项预览功能,可以在未来版本中删除,因此这些新方法被标记为弃用。

1
2
3
4
5
6
7
8
9
10
@Deprecated (forRemoval=true, since="13")
Public String stripIndent () {
}
@Deprecated (forRemoval=true, since="13")
Public String formatted (Object... Args) {

}
@Deprecated (forRemoval=true, since="13")
Public String translateEscapes () {
}
增强 Switch (引入 yield 关键字到 Switch 中)

Switch 表达式中就多了一个关键字用于跳出 Switch 块的关键字 yield,主要用于返回一个值

yieldreturn 的区别在于:return 会直接跳出当前循环或者方法,而 yield 只会跳出当前 Switch 块,同时在使用 yield 时,需要有 default 条件

1
2
3
4
5
6
7
Private static String descLanguage (String name) {
Return switch (name) {
Case "Java": yield "object-oriented, platform independent and secured";
Case "Ruby": yield "a programmer's best friend";
Default: yield name +" is a good language";
};
}

补充

关于预览特性

先贴一段 oracle 官网原文:This is a preview feature, which is a feature whose design, specification, and implementation are complete, but is not permanent, which means that the feature may exist in a different form or not at all in future JDK releases. To compile and run code that contains preview features, you must specify additional command-line options.

这是一个预览功能,该功能的设计,规格和实现是完整的,但不是永久性的,这意味着该功能可能以其他形式存在或在将来的 JDK 版本中根本不存在。要编译和运行包含预览功能的代码,必须指定其他命令行选项。

就以 switch 的增强为例子,从 Java 12 中推出,到 Java 13 中将继续增强,直到 Java 14 才正式转正进入 JDK 可以放心使用,不用考虑后续 JDK 版本对其的改动或修改

一方面可以看出 JDK 作为标准平台在增加新特性的严谨态度,另一方面个人认为是对于预览特性应该采取审慎使用的态度。特性的设计和实现容易,但是其实际价值依然需要在使用中去验证

JVM 虚拟机优化

每次 Java 版本的发布都伴随着对 JVM 虚拟机的优化,包括对现有垃圾回收算法的改进,引入新的垃圾回收算法,移除老旧的不再适用于今天的垃圾回收算法等

整体优化的方向是高效,低时延的垃圾回收表现

对于日常的应用开发者可能比较关注新的语法特性,但是从一个公司角度来说,在考虑是否升级 Java 平台时更加考虑的是JVM 运行时的提升

Java 14 & 15 新特性概览

Java 14

空指针异常精准提示

通过 JVM 参数中添加 -XX:+ShowCodeDetailsInExceptionMessages,可以在空指针异常中获取更为详细的调用信息,更快的定位和解决问题。

1
a.b.c.i = 99; // 假设这段代码会发生空指针

Java 14 之前:

1
2
Exception in thread "main" java. Lang. NullPointerException
At NullPointerExample.Main (NullPointerExample. Java:5)

Java 14 之后:

1
2
3
4
 // 增加参数后提示的异常中很明确的告知了哪里为空导致
Exception in thread "main" java. Lang. NullPointerException:
Cannot read field 'c' because 'a.b' is null.
At Prog.Main (Prog. Java:5)

Switch 的增强 (转正)

Java 12 引入的 switch(预览特性)在 Java 14 变为正式版本,不需要增加参数来启用,直接在 JDK 14 中就能使用。

Java 12 为 switch 表达式引入了类似 lambda 语法条件匹配成功后的执行块,不需要多写 break ,Java 13 提供了 yield 来在 block 中返回值。

1
2
3
4
5
6
7
8
9
10
11
12
String result = switch (day) {
Case "M", "W", "F" -> "MWF";
Case "T", "TH", "S" -> "TTS";
Default -> {
If (day.IsEmpty ())
Yield "Please insert a valid day.";
Else
Yield "Looks like a Sunday.";
}

};
System.Out.Println (result);

预览新特性

Record 关键字

record 关键字可以简化 数据类(一个 Java 类一旦实例化就不能再修改)的定义方式,使用 record 代替 class 定义的类,只需要声明属性,就可以在获得属性的访问方法,以及 toString ()hashCode (), equals () 方法。

类似于使用 class 定义类,同时使用了 lombok 插件,并打上了 @Getter,@ToString,@EqualsAndHashCode 注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 这个类具有两个特征
* 1. 所有成员属性都是 final
* 2. 全部方法由构造方法,和两个成员属性访问器组成(共三个)
* 那么这种类就很适合使用 record 来声明
*/
Final class Rectangle implements Shape {
Final double length;
Final double width;

Public Rectangle (double length, double width) {
This. Length = length;
This. Width = width;
}

Double length () { return length; }
Double width () { return width; }
}
/**
* 1. 使用 record 声明的类会自动拥有上面类中的三个方法
* 2. 在这基础上还附赠了 equals (),hashCode ()方法以及 toString ()方法
* 3. ToString 方法中包括所有成员属性的字符串表示形式及其名称
*/
Record Rectangle (float length, float width) { }
文本块

Java 14 中,文本块依然是预览特性,不过,其引入了两个新的转义字符:

  • \ : 表示行尾,不引入换行符
  • \s:表示单个空格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
String str = "凡心所向,素履所往,生如逆旅,一苇以航。";

String str 2 = """
凡心所向,素履所往, \
生如逆旅,一苇以航。""";
System.Out.Println (str 2);// 凡心所向,素履所往,生如逆旅,一苇以航。
String text = """
Java
C++\sphp
""";
System.Out.Println (text);
//输出:
Java
C++ php
Instanceof 增强

依然是预览特性Java 12 新特性中介绍过。

其他
  • 从 Java 11 引入的 ZGC 作为继 G 1 过后的下一代 GC 算法,从支持 Linux 平台到 Java 14 开始支持 MacOS 和 Windows(个人感觉是终于可以在日常开发工具中先体验下 ZGC 的效果了,虽然其实 G 1 也够用)
  • 移除了 CMS (Concurrent Mark Sweep) 垃圾收集器(功成而退)
  • 新增了 jpackage 工具,标配将应用打成 jar 包外,还支持不同平台的特性包,比如 linux 下的 debrpm,window 平台下的 msiexe

Java 15

CharSequence

CharSequence 接口添加了一个默认方法 isEmpty () 来判断字符序列为空,如果是则返回 true。

1
2
3
4
5
Public interface CharSequence {
Default boolean isEmpty () {
Return this.Length () == 0;
}
}

TreeMap

TreeMap 新引入了下面这些方法:

  • putIfAbsent ()
  • computeIfAbsent ()
  • computeIfPresent ()
  • compute ()
  • merge ()

ZGC (转正)

Java 11 的时候,ZGC 还在试验阶段。

当时,ZGC 的出现让众多 Java 开发者看到了垃圾回收器的另外一种可能,因此备受关注。

经过多个版本的迭代,不断的完善和修复问题,ZGC 在 Java 15 已经可以正式使用了!

不过,默认的垃圾回收器依然是 G 1。你可以通过下面的参数启动 ZGC:

1
$ java -XX:+UseZGC className

EdDSA (数字签名算法)

新加入了一个安全性和性能都更强的基于 Edwards-Curve Digital Signature Algorithm (EdDSA)实现的数字签名算法。

虽然其性能优于现有的 ECDSA 实现,不过,它并不会完全取代 JDK 中现有的椭圆曲线数字签名算法 ( ECDSA)。

1
2
3
4
5
6
7
8
9
10
11
12
KeyPairGenerator kpg = KeyPairGenerator.GetInstance ("Ed 25519");
KeyPair kp = kpg.GenerateKeyPair ();

Byte[] msg = "test_string".GetBytes (StandardCharsets. UTF_8);

Signature sig = Signature.GetInstance ("Ed 25519");
Sig.InitSign (kp.GetPrivate ());
Sig.Update (msg);
Byte[] s = sig.Sign ();

String encodedString = Base 64.GetEncoder (). EncodeToString (s);
System.Out.Println (encodedString);

输出:

1
0 Hc 0 lxxASZNvS 52 WsvnncJOH/mlFhnA 8 Tc 6 D/k 5 DtAX 5 BSsNVjtPF 4 R 4+yMWXVjrvB 2 mxVXmChIbki 6 goFBgAg==

文本块 (转正)

在 Java 15 ,文本块是正式的功能特性了。

隐藏类 (Hidden Classes)

隐藏类是为框架(frameworks)所设计的,隐藏类不能直接被其他类的字节码使用,只能在运行时生成类并通过反射间接使用它们。

预览新特性

密封类

密封类(Sealed Classes) 是 Java 15 中的一个预览新特性。

没有密封类之前,在 Java 中如果想让一个类不能被继承和修改,我们可以使用 final 关键字对类进行修饰。不过,这种方式不太灵活,直接把一个类的继承和修改渠道给堵死了。

密封类可以对继承或者实现它们的类进行限制,这样这个类就只能被指定的类继承。

1
2
3
4
5
6
// 抽象类 Person 只允许 Employee 和 Manager 继承。
Public abstract sealed class Person
Permits Employee, Manager {

//...
}

另外,任何扩展密封类的类本身都必须声明为 sealednon-sealedfinal

1
2
3
4
5
Public final class Employee extends Person {
}

Public non-sealed class Manager extends Person {
}

如果允许扩展的子类和封闭类在同一个源代码文件里,封闭类可以不使用 permits 语句,Java 编译器将检索源文件,在编译期为封闭类添加上许可的子类。

Instanceof 模式匹配

Java 15 并没有对此特性进行调整,继续预览特性,主要用于接受更多的使用反馈。

在未来的 Java 版本中,Java 的目标是继续完善 instanceof 模式匹配新特性。

其他

  • Nashorn JavaScript 引擎彻底移除:Nashorn 从 Java 8 开始引入的 JavaScript 引擎,Java 9 对 Nashorn 做了些增强,实现了一些 ES 6 的新特性。在 Java 11 中就已经被弃用,到了 Java 15 就彻底被删除了。
  • DatagramSocket API 重构
  • 禁用和废弃偏向锁(Biased Locking):偏向锁的引入增加了 JVM 的复杂性大于其带来的性能提升。不过,你仍然可以使用 -XX:+UseBiasedLocking 启用偏向锁定,但它会提示这是一个已弃用的 API。

Java 16 新特性概览

JEP 338: 向量 API (第一次孵化)

向量(Vector) API 最初由 JEP 338open in new window 提出,并作为孵化 APIopen in new window 集成到 Java 16 中。第二轮孵化由 JEP 414open in new window 提出并集成到 Java 17 中,第三轮孵化由 JEP 417open in new window 提出并集成到 Java 18 中,第四轮由 JEP 426open in new window 提出并集成到了 Java 19 中。

该孵化器 API 提供了一个 API 的初始迭代以表达一些向量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种指令)。尽管 HotSpot 支持自动向量化,但是可转换的标量操作集有限且易受代码更改的影响。该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。

Java 18 新特性概览 中,我有详细介绍到向量 API,这里就不再做额外的介绍了。

JEP 347: 启用 C++ 14 语言特性

Java 16 允许在 JDK 的 C++ 源代码中使用 C++14 语言特性,并提供在 HotSpot 代码中可以使用哪些特性的具体指导。

在 Java 15 中,JDK 中 C++ 代码使用的语言特性仅限于 C++98/03 语言标准。它要求更新各种平台编译器的最低可接受版本。

JEP 376: ZGC 并发线程堆栈处理

Java 16 将 ZGC 线程栈处理从安全点转移到一个并发阶段,甚至在大堆上也允许在毫秒内暂停 GC 安全点。消除 ZGC 垃圾收集器中最后一个延迟源可以极大地提高应用程序的性能和效率。

JEP 387: 弹性元空间

自从引入了 Metaspace 以来,根据反馈,Metaspace 经常占用过多的堆外内存,从而导致内存浪费。弹性元空间这个特性可将未使用的 HotSpot 类元数据(即元空间,metaspace)内存更快速地返回到操作系统,从而减少元空间的占用空间。

并且,这个提案还简化了元空间的代码以降低维护成本。

JEP 390: 对基于值的类发出警告

以下介绍摘自:实操 | 剖析 Java16 新语法特性open in new window,原文写的很不错,推荐阅读。

早在 Java 9 版本时,Java 的设计者们就对 @Deprecated 注解进行了一次升级,增加了 sinceforRemoval 等 2 个新元素。其中,since 元素用于指定标记了 @Deprecated 注解的 API 被弃用时的版本,而 forRemoval 则进一步明确了 API 标记 @Deprecated 注解时的语义,如果 forRemoval=true 时,则表示该 API 在未来版本中肯定会被删除,开发人员应该使用新的 API 进行替代,不再容易产生歧义(Java 9 之前,标记 @Deprecated 注解的 API,语义上存在多种可能性,比如:存在使用风险、可能在未来存在兼容性错误、可能在未来版本中被删除,以及应该使用更好的替代方案等)。

仔细观察原始类型的包装类(比如:java. Lang. Integerjava. Lang. Double),不难发现,其构造函数上都已经标记有 @Deprecated (since="9", forRemoval = true) 注解,这就意味着其构造函数在将来会被删除,不应该在程序中继续使用诸如 new Integer (); 这样的编码方式(建议使用 Integer a = 10; 或者 Integer.ValueOf () 函数),如果继续使用,编译期将会产生’Integer (int)’ is deprecated and marked for removal 告警。并且,值得注意的是,这些包装类型已经被指定为同 java. Util. Optionaljava. Time. LocalDateTime 一样的值类型。

其次,如果继续在 synchronized 同步块中使用值类型,将会在编译期和运行期产生警告,甚至是异常。在此大家需要注意,就算编译期和运行期没有产生警告和异常,也不建议在 synchronized 同步块中使用值类型,举个自增的例子。示例 1-5:

1
2
3
4
5
6
7
8
9
Public void inc (Integer count) {
For (int i = 0; i < 10; i++) {
New Thread (() -> {
Synchronized (count) {
Count++;
}
}). Start ();
}
}

当执行上述程序示例时,最终的输出结果一定会与你的期望产生差异,这是许多新人经常犯错的一个点,因为在并发环境下,Integer 对象根本无法通过 synchronized 来保证线程安全,这是因为每次的 count++ 操作,所产生的 hashcode 均不同,简而言之,每次加锁都锁在了不同的对象上。因此,如果希望在实际的开发过程中保证其原子性,应该使用 AtomicInteger

JEP 392: 打包工具

在 Java 14 中,JEP 343 引入了打包工具,命令是 jpackage。在 Java 15 中,继续孵化,现在在 Java 16 中,终于成为了正式功能。

这个打包工具允许打包自包含的 Java 应用程序。它支持原生打包格式,为最终用户提供自然的安装体验,这些格式包括 Windows 上的 msi 和 exe、macOS 上的 pkg 和 dmg,还有 Linux 上的 deb 和 rpm。它还允许在打包时指定启动时参数,并且可以从命令行直接调用,也可以通过 ToolProvider API 以编程方式调用。注意 jpackage 模块名称从 jdk. Incubator. Jpackage 更改为 jdk. Jpackage。这将改善最终用户在安装应用程序时的体验,并简化了“应用商店”模型的部署。

关于这个打包工具的实际使用,可以看这个视频 Playing with Java 16 jpackageopen in new window(需要梯子)。

JEP 393: 外部内存访问 API (第三次孵化)

引入外部内存访问 API 以允许 Java 程序安全有效地访问 Java 堆之外的外部内存。

Java 14 (JEP 370open in new window) 的时候,第一次孵化外部内存访问 API,Java 15 中进行了第二次复活(JEP 383open in new window),在 Java 16 中进行了第三次孵化。

引入外部内存访问 API 的目的如下:

  • 通用:单个 API 应该能够对各种外部内存(如本机内存、持久内存、堆内存等)进行操作。
  • 安全:无论操作何种内存,API 都不应该破坏 JVM 的安全性。
  • 控制:可以自由的选择如何释放内存(显式、隐式等)。
  • 可用:如果需要访问外部内存,API 应该是 sun. Misc. Unsafe.

JEP 394: instanceof 模式匹配 (转正)

JDK 版本 更新类型 JEP 更新内容
Java SE 14 preview JEP 305open in new window 首次引入 instanceof 模式匹配。
Java SE 15 Second Preview JEP 375open in new window 相比较上个版本无变化,继续收集更多反馈。
Java SE 16 Permanent Release JEP 394open in new window 模式变量不再隐式为 final。

从 Java 16 开始,你可以对 instanceof 中的变量值进行修改。

1
2
3
4
5
6
7
8
9
10
// Old code
If (o instanceof String) {
String s = (String) o;
... Use s ...
}

// New code
If (o instanceof String s) {
... Use s ...
}

JEP 395: 记录类型 (转正)

记录类型变更历史:

JDK 版本 更新类型 JEP 更新内容
Java SE 14 Preview JEP 359open in new window 引入 record 关键字,record 提供一种紧凑的语法来定义类中的不可变数据。
Java SE 15 Second Preview JEP 384open in new window 支持在局部方法和接口中使用 record
Java SE 16 Permanent Release JEP 395open in new window 非静态内部类可以定义非常量的静态成员。

从 Java SE 16 开始,非静态内部类可以定义非常量的静态成员。

1
2
3
4
5
Public class Outer {
Class Inner {
Static int age;
}
}

在 JDK 16 之前,如果写上面这种代码,IDE 会提示你静态字段 age 不能在非静态的内部类中定义,除非它用一个常量表达式初始化。(The field age cannot be declared static in a non-static inner type, unless initialized with a constant expression)

JEP 396: 默认强封装 JDK 内部元素

此特性会默认强封装 JDK 的所有内部元素,但关键内部 API(例如 sun. Misc. Unsafe)除外。默认情况下,使用早期版本成功编译的访问 JDK 内部 API 的代码可能不再起作用。鼓励开发人员从使用内部元素迁移到使用标准 API 的方法上,以便他们及其用户都可以无缝升级到将来的 Java 版本。强封装由 JDK 9 的启动器选项–illegal-access 控制,到 JDK 15 默认改为 warning,从 JDK 16 开始默认为 deny。(目前)仍然可以使用单个命令行选项放宽对所有软件包的封装,将来只有使用–add-opens 打开特定的软件包才行。

JEP 397: 密封类 (预览)

密封类由 JEP 360open in new window 提出预览,集成到了 Java 15 中。在 JDK 16 中,密封类得到了改进(更加严格的引用检查和密封类的继承关系),由 JEP 397open in new window 提出了再次预览。

Java 14 & 15 新特性概览 中,我有详细介绍到密封类,这里就不再做额外的介绍了。

其他优化与改进

  • JEP 380: Unix-Domain 套接字通道:Unix-domain 套接字一直是大多数 Unix 平台的一个特性,现在在 Windows 10 和 Windows Server 2019 也提供了支持。此特性为 java. Nio. Channels 包的套接字通道和服务器套接字通道 API 添加了 Unix-domain(AF_UNIX)套接字支持。它扩展了继承的通道机制以支持 Unix-domain 套接字通道和服务器套接字通道。Unix-domain 套接字用于同一主机上的进程间通信(IPC)。它们在很大程度上类似于 TCP/IP,区别在于套接字是通过文件系统路径名而不是 Internet 协议(IP)地址和端口号寻址的。对于本地进程间通信,Unix-domain 套接字比 TCP/IP 环回连接更安全、更有效
  • JEP 389: 外部链接器 API (孵化): 该孵化器 API 提供了静态类型、纯 Java 访问原生代码的特性,该 API 将大大简化绑定原生库的原本复杂且容易出错的过程。Java 1.1 就已通过 Java 原生接口(JNI)支持了原生方法调用,但并不好用。Java 开发人员应该能够为特定任务绑定特定的原生库。它还提供了外来函数支持,而无需任何中间的 JNI 粘合代码。
  • JEP 357: 从 Mercurial 迁移到 Git:在此之前,OpenJDK 源代码是使用版本管理工具 Mercurial 进行管理,现在迁移到了 Git。
  • JEP 369: 迁移到 GitHub:和 JEP 357 从 Mercurial 迁移到 Git 的改变一致,在把版本管理迁移到 Git 之后,选择了在 GitHub 上托管 OpenJDK 社区的 Git 仓库。不过只对 JDK 11 以及更高版本 JDK 进行了迁移。
  • JEP 386: 移植 Alpine Linux:Alpine Linux 是一个独立的、非商业的 Linux 发行版,它十分的小,一个容器需要不超过 8 MB 的空间,最小安装到磁盘只需要大约 130 MB 存储空间,并且十分的简单,同时兼顾了安全性。此提案将 JDK 移植到了 Apline Linux,由于 Apline Linux 是基于 musl lib 的轻量级 Linux 发行版,因此其他 x 64 和 AArch 64 架构上使用 musl lib 的 Linux 发行版也适用。
  • JEP 388: Windows/AArch 64 移植:这些 JEP 的重点不是移植工作本身,而是将它们集成到 JDK 主线存储库中;JEP 386 将 JDK 移植到 Alpine Linux 和其他使用 musl 作为 x 64 上主要 C 库的发行版上。此外,JEP 388 将 JDK 移植到 Windows AArch 64(ARM 64)。

Java 17 新特性概览(重要)

Java 17 在 2021 年 9 月 14 日正式发布,是一个长期支持(LTS)版本。

下面这张图是 Oracle 官方给出的 Oracle JDK 支持的时间线。可以看得到,Java

17 最多可以支持到 2029 年 9 月份。

Java 17 将是继 Java 8 以来最重要的长期支持(LTS)版本,是 Java 社区八年努力的成果。Spring 6. X 和 Spring Boot 3. X 最低支持的就是 Java 17。

这次更新共带来 14 个新特性:

这里只对 356、398、413、406、407、409、410、411、412、414 这几个我觉得比较重要的新特性进行详细介绍。

相关阅读:OpenJDK Java 17 文档open in new window

JEP 356: 增强的伪随机数生成器

JDK 17 之前,我们可以借助 RandomThreadLocalRandomSplittableRandom 来生成随机数。不过,这 3 个类都各有缺陷,且缺少常见的伪随机算法支持。

Java 17 为伪随机数生成器 (pseudorandom number generator,PRNG,又称为确定性随机位生成器)增加了新的接口类型和实现,使得开发者更容易在应用程序中互换使用各种 PRNG 算法。

PRNGopen in new window 用来生成接近于绝对随机数序列的数字序列。一般来说,PRNG 会依赖于一个初始值,也称为种子,来生成对应的伪随机数序列。只要种子确定了,PRNG 所生成的随机数就是完全确定的,因此其生成的随机数序列并不是真正随机的。

使用示例:

1
2
3
4
5
RandomGeneratorFactory<RandomGenerator> l 128 X 256 MixRandom = RandomGeneratorFactory.Of ("L 128 X 256 MixRandom");
// 使用时间戳作为随机数种子
RandomGenerator randomGenerator = l 128 X 256 MixRandom.Create (System.CurrentTimeMillis ());
// 生成随机数
RandomGenerator.NextInt (10);

JEP 398: 弃用 Applet API 以进行删除

Applet API 用于编写在 Web 浏览器端运行的 Java 小程序,很多年前就已经被淘汰了,已经没有理由使用了。

Applet API 在 Java 9 时被标记弃用(JEP 289open in new window),但不是为了删除。

JEP 406: switch 的类型匹配(预览)

正如 instanceof 一样, switch 也紧跟着增加了类型匹配自动转换功能。

instanceof 代码示例:

1
2
3
4
5
6
7
8
9
10
// Old code
If (o instanceof String) {
String s = (String) o;
... Use s ...
}

// New code
If (o instanceof String s) {
... Use s ...
}

switch 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Old code
Static String formatter (Object o) {
String formatted = "unknown";
If (o instanceof Integer i) {
Formatted = String.Format ("int %d", i);
} else if (o instanceof Long l) {
Formatted = String.Format ("long %d", l);
} else if (o instanceof Double d) {
Formatted = String.Format ("double %f", d);
} else if (o instanceof String s) {
Formatted = String.Format ("String %s", s);
}
Return formatted;
}

// New code
Static String formatterPatternSwitch (Object o) {
Return switch (o) {
Case Integer i -> String.Format ("int %d", i);
Case Long l -> String.Format ("long %d", l);
Case Double d -> String.Format ("double %f", d);
Case String s -> String.Format ("String %s", s);
default -> o.toString ();
};
}

对于 null 值的判断也进行了优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Old code
Static void testFooBar (String s) {
If (s == null) {
System.Out.Println ("oops!");
Return;
}
Switch (s) {
Case "Foo", "Bar" -> System.Out.Println ("Great");
Default -> System.Out.Println ("Ok");
}
}

// New code
Static void testFooBar (String s) {
Switch (s) {
Case null -> System.Out.Println ("Oops");
Case "Foo", "Bar" -> System.Out.Println ("Great");
Default -> System.Out.Println ("Ok");
}
}

JEP 407: 删除远程方法调用激活机制

删除远程方法调用 (RMI) 激活机制,同时保留 RMI 的其余部分。RMI 激活机制已过时且不再使用。

JEP 409: 密封类(转正)

密封类由 JEP 360open in new window 提出预览,集成到了 Java 15 中。在 JDK 16 中,密封类得到了改进(更加严格的引用检查和密封类的继承关系),由 JEP 397open in new window 提出了再次预览。

Java 14 & 15 新特性概览 中,我有详细介绍到密封类,这里就不再做额外的介绍了。

JEP 410: 删除实验性的 AOT 和 JIT 编译器

在 Java 9 的 JEP 295open in new window ,引入了实验性的提前 (AOT) 编译器,在启动虚拟机之前将 Java 类编译为本机代码。

Java 17,删除实验性的提前 (AOT) 和即时 (JIT) 编译器,因为该编译器自推出以来很少使用,维护它所需的工作量很大。保留实验性的 Java 级 JVM 编译器接口 (JVMCI),以便开发人员可以继续使用外部构建的编译器版本进行 JIT 编译。

JEP 411: 弃用安全管理器以进行删除

弃用安全管理器以便在将来的版本中删除。

安全管理器可追溯到 Java 1.0,多年来,它一直不是保护客户端 Java 代码的主要方法,也很少用于保护服务器端代码。为了推动 Java 向前发展,Java 17 弃用安全管理器,以便与旧版 Applet API ( JEP 398open in new window ) 一起移除。

JEP 412: 外部函数和内存 API(孵化)

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412open in new window 提出。第二轮孵化由 JEP 419open in new window 提出并集成到了 Java 18 中,预览由 JEP 424open in new window 提出并集成到了 Java 19 中。

Java 19 新特性概览 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。

JEP 414: 向量 API(第二次孵化)

向量(Vector) API 最初由 JEP 338open in new window 提出,并作为孵化 APIopen in new window 集成到 Java 16 中。第二轮孵化由 JEP 414open in new window 提出并集成到 Java 17 中,第三轮孵化由 JEP 417open in new window 提出并集成到 Java 18 中,第四轮由 JEP 426open in new window 提出并集成到了 Java 19 中。

该孵化器 API 提供了一个 API 的初始迭代以表达一些向量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种指令)。尽管 HotSpot 支持自动向量化,但是可转换的标量操作集有限且易受代码更改的影响。该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。

Java 18 新特性概览

Java 18 在 2022 年 3 月 22 日正式发布,非长期支持版本。

Java 18 带来了 9 个新特性:

Java 17 中包含 14 个特性,Java 16 中包含 17 个特性,Java 15 中包含 14 个特性,Java 14 中包含 16 个特性。相比于前面发布的版本来说,Java 18 的新特性少了很多。

这里只对 400、408、413、416、417、418、419 这几个我觉得比较重要的新特性进行详细介绍。

相关阅读:

JEP 400: 默认字符集为 UTF-8

JDK 终于将 UTF-8 设置为默认字符集。

在 Java 17 及更早版本中,默认字符集是在 Java 虚拟机运行时才确定的,取决于不同的操作系统、区域设置等因素,因此存在潜在的风险。就比如说你在 Mac 上运行正常的一段打印文字到控制台的 Java 程序到了 Windows 上就会出现乱码,如果你不手动更改字符集的话。

JEP 408: 简易的 Web 服务器

Java 18 之后,你可以使用 jwebserver 命令启动一个简易的静态 Web 服务器。

1
2
3
4
$ jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b :: ".
Serving /cwd and subdirectories on 127.0.0.1 port 8000
URL: http://127.0.0.1:8000/

这个服务器不支持 CGI 和 Servlet,只限于静态文件。

JEP 413: 优化 Java API 文档中的代码片段

在 Java 18 之前,如果我们想要在 Javadoc 中引入代码片段可以使用 <pre>{@code ...}</pre>

1
2
3
<pre>{@code
Lines of source code
}</pre>

<pre>{@code ...}</pre> 这种方式生成的效果比较一般。

在 Java 18 之后,可以通过 @snippet 标签来做这件事情。

1
2
3
4
5
6
7
8
/**
* The following code shows how to use {@code Optional. IsPresent}:
* {@snippet :
* if (v.isPresent ()) {
* System.Out.Println ("v: " + v.get ());
* }
* }
*/

@snippet 这种方式生成的效果更好且使用起来更方便一些。

JEP 416: 使用方法句柄重新实现反射核心

Java 18 改进了 java. Lang. Reflect. MethodConstructor 的实现逻辑,使之性能更好,速度更快。这项改动不会改动相关 API ,这意味着开发中不需要改动反射相关代码,就可以体验到性能更好反射。

OpenJDK 官方给出了新老实现的反射性能基准测试结果。

新老实现的反射性能基准测试结果

新老实现的反射性能基准测试结果

JEP 417: 向量 API(第三次孵化)

向量(Vector) API 最初由 JEP 338open in new window 提出,并作为孵化 APIopen in new window 集成到 Java 16 中。第二轮孵化由 JEP 414open in new window 提出并集成到 Java 17 中,第三轮孵化由 JEP 417open in new window 提出并集成到 Java 18 中,第四轮由 JEP 426open in new window 提出并集成到了 Java 19 中。

向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。

向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。

这是对数组元素的简单标量计算:

1
2
3
4
5
Void scalarComputation (float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
C[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0 f;
}
}

这是使用 Vector API 进行的等效向量计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static final VectorSpecies<Float> SPECIES = FloatVector. SPECIES_PREFERRED;

Void vectorComputation (float[] a, float[] b, float[] c) {
Int i = 0;
int upperBound = SPECIES.LoopBound (a.length);
For (; i < upperBound; i += SPECIES.Length ()) {
// FloatVector va, vb, vc;
Var va = FloatVector.FromArray (SPECIES, a, i);
Var vb = FloatVector.FromArray (SPECIES, b, i);
Var vc = va.Mul (va)
.add (vb.Mul (vb))
.neg ();
Vc.IntoArray (c, i);
}
for (; i < a.length; i++) {
C[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0 f;
}
}

在 JDK 18 中,向量 API 的性能得到了进一步的优化。

JEP 418: 互联网地址解析 SPI

Java 18 定义了一个全新的 SPI(service-provider interface),用于主要名称和地址的解析,以便 java. Net. InetAddress 可以使用平台之外的第三方解析器。

JEP 419: Foreign Function & Memory API(第二次孵化)

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412open in new window 提出。第二轮孵化由 JEP 419open in new window 提出并集成到了 Java 18 中,预览由 JEP 424open in new window 提出并集成到了 Java 19 中。

Java 19 新特性概览

JDK 19 定于 2022 年 9 月 20 日正式发布以供生产使用,非长期支持版本。不过,JDK 19 中有一些比较重要的新特性值得关注。

JDK 19 只有 7 个新特性:

这里只对 424、425、426、428 这 4 个我觉得比较重要的新特性进行详细介绍。

相关阅读:OpenJDK Java 19 文档open in new window

JEP 424: 外部函数和内存 API(预览)

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412open in new window 提出。第二轮孵化由 JEP 419open in new window 提出并集成到了 Java 18 中,预览由 JEP 424open in new window 提出并集成到了 Java 19 中。

在没有外部函数和内存 API 之前:

  • Java 通过 sun.misc.Unsafeopen in new window 提供一些执行低级别、不安全操作的方法(如直接访问系统内存资源、自主管理内存资源等),Unsafe 类让 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力的同时,也增加了 Java 语言的不安全性,不正确使用 Unsafe 类会使得程序出错的概率变大。
  • Java 1.1 就已通过 Java 原生接口(JNI)支持了原生方法调用,但并不好用。JNI 实现起来过于复杂,步骤繁琐(具体的步骤可以参考这篇文章:Guide to JNI (Java Native Interface)open in new window ),不受 JVM 的语言安全机制控制,影响 Java 语言的跨平台特性。并且,JNI 的性能也不行,因为 JNI 方法调用不能从许多常见的 JIT 优化 (如内联)中受益。虽然 JNAopen in new windowJNRopen in new windowJavaCPPopen in new window 等框架对 JNI 进行了改进,但效果还是不太理想。

引入外部函数和内存 API 就是为了解决 Java 访问外部函数和外部内存存在的一些痛点。

Foreign Function & Memory API (FFM API) 定义了类和接口:

  • 分配外部内存:MemorySegment、、MemoryAddressSegmentAllocator);
  • 操作和访问结构化的外部内存:MemoryLayout, VarHandle
  • 控制外部内存的分配和释放:MemorySession
  • 调用外部函数:LinkerFunctionDescriptorSymbolLookup

下面是 FFM API 使用示例,这段代码获取了 C 库函数的 radixsort 方法句柄,然后使用它对 Java 数组中的四个字符串进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 在 C 库路径上查找外部函数
Linker linker = Linker.NativeLinker ();
SymbolLookup stdlib = linker.DefaultLookup ();
MethodHandle radixSort = linker.DowncallHandle (
Stdlib.Lookup ("radixsort"), ...);
// 2. 分配堆上内存以存储四个字符串
String[] javaStrings = { "mouse", "cat", "dog", "car" };
// 3. 分配堆外内存以存储四个指针
SegmentAllocator allocator = implicitAllocator ();
MemorySegment offHeap = allocator.AllocateArray (ValueLayout. ADDRESS, javaStrings. Length);
// 4. 将字符串从堆上复制到堆外
For (int i = 0; i < javaStrings. Length; i++) {
// 在堆外分配一个字符串,然后存储指向它的指针
MemorySegment cString = allocator. AllocateUtf 8 String (javaStrings[i]);
OffHeap.SetAtIndex (ValueLayout. ADDRESS, i, cString);
}
// 5. 通过调用外部函数对堆外数据进行排序
RadixSort.Invoke (offHeap, javaStrings. Length, MemoryAddress. NULL, '\0');
// 6. 将 (重新排序的)字符串从堆外复制到堆上
For (int i = 0; i < javaStrings. Length; i++) {
MemoryAddress cStringPtr = offHeap.GetAtIndex (ValueLayout. ADDRESS, i);
JavaStrings[i] = cStringPtr. GetUtf 8 String (0);
}
Assert Arrays.Equals (javaStrings, new String[] {"car", "cat", "dog", "mouse"}); // true

JEP 425: 虚拟线程(预览)

虚拟线程(Virtual Thread-)是 JDK 而不是 OS 实现的轻量级线程 (Lightweight Process,LWP),许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。

虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。

虚拟线程避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。

知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看: https://www.zhihu.com/question/536743167

Java 虚拟线程的详细解读和原理可以看下面这两篇文章:

JEP 426: 向量 API(第四次孵化)

向量(Vector) API 最初由 JEP 338open in new window 提出,并作为孵化 APIopen in new window 集成到 Java 16 中。第二轮孵化由 JEP 414open in new window 提出并集成到 Java 17 中,第三轮孵化由 JEP 417open in new window 提出并集成到 Java 18 中,第四轮由 JEP 426open in new window 提出并集成到了 Java 19 中。

Java 18 新特性概览 中,我有详细介绍到向量 API,这里就不再做额外的介绍了。

JEP 428: 结构化并发 (孵化)

JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代 java. Util. Concurrent,目前处于孵化器阶段。

结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。

结构化并发的基本 API 是 StructuredTaskScopeopen in new windowStructuredTaskScope 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。

StructuredTaskScope 的基本用法如下:

1
2
3
4
5
6
7
8
9
try (var scope = new StructuredTaskScope<Object>()) {
// 使用 fork 方法派生线程来执行子任务
Future<Integer> future 1 = scope.Fork (task 1);
Future<String> future 2 = scope.Fork (task 2);
// 等待线程完成
Scope.Join ();
// 结果的处理可能包括处理或重新抛出异常
... Process results/exceptions ...
} // close

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。

Java 20 新特性概览

JDK 20 于 2023 年 3 月 21 日发布,非长期支持版本。

根据开发计划,下一个 LTS 版本就是将于 2023 年 9 月发布的 JDK 21。

JDK 20 只有 7 个新特性:

JEP 429:作用域值(第一次孵化)

作用域值(Scoped Values)它可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。

1
2
3
4
5
6
7
8
final static ScopedValue<...> V = new ScopedValue<>();

// In some method
ScopedValue.Where (V, <value>)
.run (() -> { ... V.get () ... Call methods ... });

// In a method called directly or indirectly from the lambda expression
... V.get () ...

作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。

关于作用域值的详细介绍,推荐阅读作用域值常见问题解答open in new window 这篇文章。

JEP 432:记录模式(第二次预览)

记录模式(Record Patterns) 可对 record 的值进行解构,也就是更方便地从记录类(Record Class)中提取数据。并且,还可以嵌套记录模式和类型模式结合使用,以实现强大的、声明性的和可组合的数据导航和处理形式。

记录模式不能单独使用,而是要与 instanceof 或 switch 模式匹配一同使用。

先以 instanceof 为例简单演示一下。

简单定义一个记录类:

1
Record Shape (String type, long unit){}

没有记录模式之前:

1
2
3
4
5
Shape circle = new Shape ("Circle", 10);
If (circle instanceof Shape shape) {

System.Out.Println ("Area of " + shape.Type () + " is : " + Math. PI * Math.Pow (shape.Unit (), 2));
}

有了记录模式之后:

1
2
3
4
Shape circle = new Shape ("Circle", 10);
If (circle instanceof Shape (String type, long unit)) {
System.Out.Println ("Area of " + type + " is : " + Math. PI * Math.Pow (unit, 2));
}

再看看记录模式与 switch 的配合使用。

定义一些类:

1
2
3
4
Interface Shape {}
Record Circle (double radius) implements Shape { }
Record Square (double side) implements Shape { }
Record Rectangle (double length, double width) implements Shape { }

没有记录模式之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Shape shape = new Circle (10);
Switch (shape) {
Case Circle c:
System.Out.Println ("The shape is Circle with area: " + Math. PI * c.radius () * c.radius ());
Break;

Case Square s:
System.Out.Println ("The shape is Square with area: " + s.side () * s.side ());
Break;

Case Rectangle r:
System.Out.Println ("The shape is Rectangle with area: + " + r.length () * r.width ());
Break;

Default:
System.Out.Println ("Unknown Shape");
Break;
}

有了记录模式之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Shape shape = new Circle (10);
Switch (shape) {

Case Circle (double radius):
System.Out.Println ("The shape is Circle with area: " + Math. PI * radius * radius);
Break;

Case Square (double side):
System.Out.Println ("The shape is Square with area: " + side * side);
Break;

Case Rectangle (double length, double width):
System.Out.Println ("The shape is Rectangle with area: + " + length * width);
Break;

Default:
System.Out.Println ("Unknown Shape");
Break;
}

记录模式可以避免不必要的转换,使得代码更建简洁易读。而且,用了记录模式后不必再担心 null 或者 NullPointerException,代码更安全可靠。

记录模式在 Java 19 进行了第一次预览,由 JEP 405open in new window 提出。JDK 20 中是第二次预览,由 JEP 432open in new window 提出。这次的改进包括:

  • 添加对通用记录模式类型参数推断的支持,
  • 添加对记录模式的支持以出现在增强语句的标题中 for
  • 删除对命名记录模式的支持。

注意:不要把记录模式和 JDK16 正式引入的记录类搞混了。

JEP 433:switch 模式匹配(第四次预览)

正如 instanceof 一样, switch 也紧跟着增加了类型匹配自动转换功能。

instanceof 代码示例:

1
2
3
4
5
6
7
8
9
10
// Old code
If (o instanceof String) {
String s = (String) o;
... Use s ...
}

// New code
If (o instanceof String s) {
... Use s ...
}

switch 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Old code
Static String formatter (Object o) {
String formatted = "unknown";
If (o instanceof Integer i) {
Formatted = String.Format ("int %d", i);
} else if (o instanceof Long l) {
Formatted = String.Format ("long %d", l);
} else if (o instanceof Double d) {
Formatted = String.Format ("double %f", d);
} else if (o instanceof String s) {
Formatted = String.Format ("String %s", s);
}
Return formatted;
}

// New code
Static String formatterPatternSwitch (Object o) {
Return switch (o) {
Case Integer i -> String.Format ("int %d", i);
Case Long l -> String.Format ("long %d", l);
Case Double d -> String.Format ("double %f", d);
Case String s -> String.Format ("String %s", s);
default -> o.toString ();
};
}

switch 模式匹配分别在 Java 17、Java 18、Java 19 中进行了预览,Java 20 是第四次预览了。每一次的预览基本都会有一些小改进,这里就不细提了。

JEP 434: 外部函数和内存 API(第二次预览)

Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。

外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 JEP 412open in new window 提出。Java 18 中进行了第二次孵化,由 JEP 419open in new window 提出。Java 19 中是第一次预览,由 JEP 424open in new window 提出。

JDK 20 中是第二次预览,由 JEP 434open in new window 提出,这次的改进包括:

  • MemorySegmentMemoryAddress 抽象的统一
  • 增强的 MemoryLayout 层次结构
  • MemorySession 拆分为 ArenaSegmentScope,以促进跨维护边界的段共享。

Java 19 新特性概览 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。

JEP 436: 虚拟线程(第二次预览)

虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程 (Lightweight Process,LWP),由 JVM 调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。

在引入虚拟线程之前,java. Lang. Thread 包已经支持所谓的平台线程,也就是没有虚拟线程之前,我们一直使用的线程。JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上),当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程。

虚拟线程、平台线程和系统内核线程的关系图如下所示(图源:How to Use Java 19 Virtual Threadsopen in new window):

虚拟线程、平台线程和系统内核线程的关系

虚拟线程、平台线程和系统内核线程的关系

关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统是一个特例,HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: JVM 中的线程模型是用户级的么?open in new window

相比较于平台线程来说,虚拟线程是廉价且轻量级的,使用完后立即被销毁,因此它们不需要被重用或池化,每个任务可以有自己专属的虚拟线程来运行。虚拟线程暂停和恢复来实现线程之间的切换,避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。

虚拟线程在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。

知乎有一个关于 Java 19 虚拟线程的讨论,感兴趣的可以去看看:https://www.zhihu.com/question/536743167open in new window

Java 虚拟线程的详细解读和原理可以看下面这两篇文章:

虚拟线程在 Java 19 中进行了第一次预览,由 JEP 425open in new window 提出。JDK 20 中是第二次预览,做了一些细微变化,这里就不细提了。

最后,我们来看一下四种创建虚拟线程的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 1、通过 Thread.OfVirtual () 创建
Runnable fn = () -> {
// your code here
};

Thread thread = Thread.OfVirtual (fn)
.start ();

// 2、通过 Thread.StartVirtualThread () 、创建
Thread thread = Thread.StartVirtualThread (() -> {
// your code here
});

// 3、通过 Executors.NewVirtualThreadPerTaskExecutor () 创建
Var executorService = Executors.NewVirtualThreadPerTaskExecutor ();

ExecutorService.Submit (() -> {
// your code here
});

Class CustomThread implements Runnable {
@Override
Public void run () {
System.Out.Println ("CustomThread run");
}
}

//4、通过 ThreadFactory 创建
CustomThread customThread = new CustomThread ();
// 获取线程工厂类
ThreadFactory factory = Thread.OfVirtual (). Factory ();
// 创建虚拟线程
Thread thread = factory.NewThread (customThread);
// 启动线程
Thread.Start ();

通过上述列举的 4 种创建虚拟线程的方式可以看出,官方为了降低虚拟线程的门槛,尽力复用原有的 Thread 线程类,这样可以平滑的过渡到虚拟线程的使用。

JEP 437: 结构化并发 (第二次孵化)

Java 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代 java. Util. Concurrent,目前处于孵化器阶段。

结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。

结构化并发的基本 API 是 StructuredTaskScopeopen in new windowStructuredTaskScope 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。

StructuredTaskScope 的基本用法如下:

1
2
3
4
5
6
7
8
9
try (var scope = new StructuredTaskScope<Object>()) {
// 使用 fork 方法派生线程来执行子任务
Future<Integer> future 1 = scope.Fork (task 1);
Future<String> future 2 = scope.Fork (task 2);
// 等待线程完成
Scope.Join ();
// 结果的处理可能包括处理或重新抛出异常
... Process results/exceptions ...
} // close

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。

JDK 20 中对结构化并发唯一变化是更新为支持在任务范围内创建的线程 StructuredTaskScope 继承范围值这简化了跨线程共享不可变数据,详见 JEP 429open in new window

JEP 432:向量 API(第五次孵化)

向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。

向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。

向量(Vector) API 最初由 JEP 338open in new window 提出,并作为孵化 APIopen in new window 集成到 Java 16 中。第二轮孵化由 JEP 414open in new window 提出并集成到 Java 17 中,第三轮孵化由 JEP 417open in new window 提出并集成到 Java 18 中,第四轮由 JEP 426open in new window 提出并集成到了 Java 19 中。

Java 20 的这次孵化基本没有改变向量 API ,只是进行了一些错误修复和性能增强,详见 JEP 438open in new window


Java 新特性
https://hexo.leelurker.com/posts/31630
作者
LeeLurker
发布于
2024年1月3日
许可协议