Java9的主要新特性总结

993 阅读32分钟

还在用java8,java23都快出来了,赶紧学习一下java8以上的版本吧!

概述

JDK 9 于 2017 年 9 月 17 日正式发布。

JEP(Java Enhancement Proposal)Java增强提案

变动说明

官网:

docs.oracle.com/javase/9/wh…

docs.oracle.com/javase/9/la…

openjdk.org/projects/jd…

更多内容:docs.oracle.com/javase/9/

重要变更和信息

JDK 9 包含 91 个 新特性 ,其中比较重要的有:

下载地址

您可以从这个链接下载生产就绪的OpenJDK版本。文件为压缩包,解压并设置环境变量就可以使用。

当然你也可以从这个链接下载Oracle JDK版本(但是需要注意商用限制),更多版本下载

Java9新特性总结

1、JEP 222:交互式编程环境Jshell

JEP 222

交互式编程环境是一种让程序员能够即时输入代码并立即获得反馈的开发环境。每输入一行代码,系统就会立刻执行并显示结果,使得用户可以快速验证想法、进行简单计算等操作。尽管这种环境不太适合处理复杂的工程需求,但在快速验证和简单计算等场景下非常实用。尽管其他高级编程语言(比如Python)早就拥有了交互式编程环境,Java直到Java 9才正式推出了类似的工具。

下面就来一起学习下,这个Java中的交互式编程环境Jshell。

使用

启动Jshell

打开终端,然后执行命令:jshell,执行效果如下:我这里以java11为例

 F:\Program Files\Java\jdk-11.0.12\bin>jshell
 |  欢迎使用 JShell -- 版本 11.0.12
 |  要大致了解该版本, 请键入: /help intro
 ​
 jshell>

执行计算

在jshell中可以快速的执行计算操作并获得结果,比如这样:

 jshell> 1+1
 $1 ==> 2
 # 注意上面的返回,这是里一个临时变量。下面是定义变量计算的情况,注意返回结果的不同。
 jshell> int c = 1+1
 c ==> 2
 ​
 jshell> 2*3
 $2 ==> 6
 # 注意char类型和String类型的区别
 jshell>  'a'+'b' 
 $3 ==> 195 jshell> "a"+"b"
 $4 ==> "ab"

定义变量

在jshell中也可以定义变量与方法:

 jshell> int a=1, b=2;
 a ==> 1
 b ==> 2
 ​
 jshell> a+b
 $7 ==> 3

定义方法

在jshell中也可以函数来封装操作,比如下面就是一个定义求和函数并调用它的例子:

 jshell> int sum(int a, int b){
    ...>     return a + b;
    ...> }
 |  已创建 方法 sum(int,int)
 ​
 jshell> sum(1,2)
 $10 ==> 3

定义类

既然变量和方法都可以定义,那么合理推测,在jshell中,定义类,也是可以的,上示例:

 jshell> public class MyCls {
    ...>     public int a;
    ...>     public int b;
    ...>
    ...>     public int add(){
    ...>         return a+b;
    ...>     }
    ...>
    ...>     public int mul(){
    ...>         return a*b;
    ...>     }
    ...>
    ...> }
 |  已创建 类 MyCls
 ​
 jshell> MyCls m = new MyCls()
 m ==> MyCls@39c0f4a
 ​
 jshell> m.a = 2
 $21 ==> 2
 ​
 jshell> m.b = 3
 $22 ==> 3
 # 相加
 jshell> m.add()
 $23 ==> 5
 # 相乘
 jshell> m.mul()
 $24 ==> 6

帮助命令:/help

关于jshell常用命令,我们可以通过/help来查看

 jshell> /help
 |  键入 Java 语言表达式, 语句或声明。
 |  或者键入以下命令之一:
 |  /list [<名称或 id>|-all|-start]
 |       列出您键入的源
 |  /edit <名称或 id>
 |       编辑源条目
 |  /drop <名称或 id>
 |       删除源条目
 |  /save [-all|-history|-start] <文件>
 |       将片段源保存到文件
 |  /open <file>
 |       打开文件作为源输入
 |  /vars [<名称或 id>|-all|-start]
 |       列出已声明变量及其值
 |  /methods [<名称或 id>|-all|-start]
 |       列出已声明方法及其签名
 |  /types [<名称或 id>|-all|-start]
 |       列出类型声明
 |  /imports
 |       列出导入的项
 |  /exit [<integer-expression-snippet>]
 |       退出 jshell 工具
 |  /env [-class-path <路径>] [-module-path <路径>] [-add-modules <模块>] ...
 |       查看或更改评估上下文
 |  /reset [-class-path <路径>] [-module-path <路径>] [-add-modules <模块>]...
 |       重置 jshell 工具
 |  /reload [-restore] [-quiet] [-class-path <路径>] [-module-path <路径>]...
 |       重置和重放相关历史记录 -- 当前历史记录或上一个历史记录 (-restore)
 |  /history [-all]
 |       您键入的内容的历史记录
 |  /help [<command>|<subject>]
 |       获取有关使用 jshell 工具的信息
 |  /set editor|start|feedback|mode|prompt|truncation|format ...
 |       设置配置信息
 |  /? [<command>|<subject>]
 |       获取有关使用 jshell 工具的信息
 |  /!
 |       重新运行上一个片段 -- 请参阅 /help rerun
 |  /<id>
 |       按 ID 或 ID 范围重新运行片段 -- 参见 /help rerun
 |  /-<n>
 |       重新运行以前的第 n 个片段 -- 请参阅 /help rerun
 |
 |  有关详细信息, 请键入 '/help', 后跟
 |  命令或主题的名称。
 |  例如 '/help /list''/help intro'。主题:
 |
 |  intro
 |       jshell 工具的简介
 |  keys
 |       类似 readline 的输入编辑的说明
 |  id
 |       片段 ID 以及如何使用它们的说明
 |  shortcuts
 |       片段和命令输入提示, 信息访问以及
 |       自动代码生成的按键说明
 |  context
 |       /env /reload 和 /reset 的评估上下文选项的说明
 |  rerun
 |       重新评估以前输入片段的方法的说明

查看定义的变量:/vars

 jshell> /vars
 |    int $1 = 2
 |    int $2 = 6
 |    int $3 = 195
 |    String $4 = "ab"
 |    int a = 1
 |    int b = 2
 |    int $7 = 3
 |    int $10 = 3
 |    int c = 2
 |    int $15 = 2
 |    MyCls m = MyCls@39c0f4a
 |    int $21 = 2
 |    int $22 = 3
 |    int $23 = 5
 |    int $24 = 6

查看定义的函数:/methods

 jshell> /methods
 |    int sum(int,int)

查看定义的类:/types

jshell> /types
|    class MyCls

列出输入源条目:/list

这个命令看到之前在jshell中输入的所有执行内容:

jshell> /list

   1 : 1+1
   2 : 2*3
   3 : 'a'+'b'
   4 : "a"+"b"
   5 : int a=1, b=2;
   6 : int a=1, b=2;
   7 : a+b
   9 : int sum(int a,int b){
           return a+b;
       }
  10 : sum(1,2)
  12 : int c = 1+1;
  19 : public class MyCls {
           public int a;
           public int b;

           public int add(){
               return a+b;
           }

           public int mul(){
               return a*b;
           }

       }
  20 : MyCls m = new MyCls();
  21 : m.a = 2
  22 : m.b = 3
  23 : m.add()
  24 : m.mul()

左侧的数字为条目id,可以利用该id,进行编辑和删除操作

编辑源条目:/edit

通过/list列出了所有输入的条目信息,下面我们通过/edit修改一下第一条内容:

jshell> /edit 1

此时会弹出修改框:

添加图片注释,不超过 140 字(可选)

修改完成后,点击Accept。此时可以看到jshell窗口的输出结果发生了变化。

添加图片注释,不超过 140 字(可选)

退出,点击Exit按钮。

删除源条目:/drop

/drop命令可以用来删除某个源条目,我们删除6条int a=1, b=2;

jshell> /drop 6
|  已删除 变量 b

执行后可以看到提示,实际上这种多个变量定义到一行上的,只会删除最后一个变量,可以试一下a还在,b已经没有了。

jshell> b
|  错误:
|  找不到符号
|    符号:   变量 b
|    位置: 类
|  b
|  ^

jshell> a
a ==> 1

保存文件:/save

如果想把这次编辑的内容保存下来,以便下次继续使用,可以通过/save来保存到文件里,比如这样:

# 路径是jdk的bin目录
jshell> /save my1.txt

打开文件:/open

当我们换了jshell环境后,可以通过打开之前保存的文件来快速还原之前的执行内容,比如:

jshell> /open my1.txt

重置jshell:/reset

当我们要换一个内容编写的时候,需要清空之前执行的条目(清空/list的内容),这个时候就可以这样来实现:

jshell> /reset
|  正在重置状态。

查看引入的包:/imports

jshell> /imports
|    import java.io.*
|    import java.math.*
|    import java.net.*
|    import java.nio.file.*
|    import java.util.*
|    import java.util.concurrent.*
|    import java.util.function.*
|    import java.util.prefs.*
|    import java.util.regex.*
|    import java.util.stream.*

退出jshell:/exit

jshell> /exit
|  再见

2、JEP 269:集合工厂方法

JEP 269

用过谷歌Guava类库的知道,Guava提供了创建不可变集合的静态工厂方法,而且能够推断泛型,举个例子:

List<String> list = ImmutableList.of("诗", "书", "礼", "易", "春秋"); 
// ImmutableSet、ImmutableMap也有类似的方法

在 Java 9 之前,要构建一个不可变集合往往需要经历若干繁琐的步骤,如初始化集合、添加元素以及对其进行封装处理。Java 9 通过引入专门的不可变集合(包括List、Set、Map)特性,旨在简化这一流程,提供了一个既简洁又安全的方法来创建不可变集合,确保了集合内容的不变性和线程安全性。

其内容包括:

  • List.of():创建一个不可变的 List,可以传递任意数量(与其他工具类注意区分)的元素,注意里面不能有null。
  • Set.of():创建一个不可变的 Set,可以传递任意数量(与其他工具类注意区分)的元素,注意里面不能有null。
  • Map.of() 和 Map.ofEntries():用于创建一个不可变的 Map。Map.of() 可以直接传递键值对,而 Map.ofEntries() 可以通过 Map.entry(k, v) 创建条目,注意key和value都能为null。 以上方法创建的集合为不可变对象,不能添加、修改、删除。

一般写法

以往我们创建一些集合的时候,通常是这样写的:


// Set
Set<String> set = new HashSet<>();
set.add("秦");
set.add("汉");
set.add("唐");
set.add("宋");
// 如果想不可变
set = Collections.unmodifiableSet(set);

// List
List<String> list = new ArrayList<>();
list.add("诗");
list.add("书");
list.add("礼");
list.add("易");
list.add("春秋");
// 如果想不可变
list = Collections.unmodifiableList(list);

Java8的写法

在Java 8中可以使用Stream API简化一下:

Set<String> set = Stream.of("秦", "汉", "唐", "宋").collect(Collectors.toSet());
List<String> list = Stream.of("诗", "书", "礼", "易", "春秋").collect(Collectors.toList());
// 如果想不可变
Set<String> set = Collections.unmodifiableSet(Stream.of("秦", "汉", "唐", "宋").collect(Collectors.toSet()));
List<String> list = Collections.unmodifiableList(Stream.of("诗", "书", "礼", "易", "春秋").collect(Collectors.toList()));

Java9的写法

到了Java 9,这一操作变的更为简单,只需要这样:

Set<String> set = Set.of("秦", "汉", "唐", "宋");
List<String> list = List.of("诗", "书", "礼", "易", "春秋");

Map类型可以这样写:

Map<String, String> map = Map.of("诗仙", "李白", "诗圣", "杜甫", "诗佛", "王维");

需要注意的是,Map.of的参数是key和value成对出现的,所以参数数量一定是偶数:

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)
Map.of(k1, v1, k2, v2, k3, v3)
...

List.of与asList的区别

我们也可以使用asList来快速创建集合

List<String> list2 = Arrays.asList(new String[] { "诗", "书", "礼", "易", "春秋" });

看起来看List.of和Arrays.asList比较类似,那他们之间除了长的不一样外,还有什么区别吗?

  1. Java 9中推出List.of创建的是不可变集合,而Arrays.asList是可变集合(长度不可变,值可变)
  2. List.of和Arrays.asList都不允许add和remove元素,但Arrays.asList可以调用set更改值,而List.of不可以,会报java.lang.UnsupportedOperationException异常
  3. List.of中不允许有null值,Arrays.asList中可以有null值

3、JEP 213:微小改动

JEP 213

接口支持私有方法

接口进化过程:

Java 8 支持接口的默认方法和静态方法》Java 9 可定义 private 私有方法。

接口中私有方法的特点:

  1. 私有方法不能定义为抽象的
  2. 私有方法只能在接口内部使用,实现该接口的类或其他外部类无法调用这些方法
  3. 私有方法不会继承给接口的子接口,每个接口都必须自己定义自己的私有方法。

1、定义接口

public interface IWriter {

    /**
     * 测试抽象方法,需要被子类实现。
     */
    void skill();

    /**
     * 测试默认实现方法。如果大部分子类都是相同的实现,可直接使用默认方法实现。
     */
    default void write() {
        System.out.println("=====这是一个默认方法====");
        System.out.println("创作ing…………");

        // 调用私有方法
        thinking();

        // 调用私有静态方法。非静态方法可以调用静态方法,反之不行。
        hideTag();
        System.out.println("创作完成…………");
    }

    /**
     * 测试私有方法
     */
    private void thinking() {
        System.out.println("=====这是一个私有方法=====");
        System.out.println("构思ing…………");
    }

    /**
     * 测试静态方法。
     */
    static void tag() {
        System.out.println("=====这是一个静态方法=====");
        System.out.println("标签是:文人");

        // 调用私有静态方法
        hideTag();

    }

    /**
     * 测试私有静态方法
     */
    private static void hideTag() {
        System.out.println("=====这是一个私有静态方法=====");
        System.out.println("隐藏标签:读书人");
    }
}

2、定义接口实现

public class TangWriter implements IWriter {
    @Override
    public void skill() {
        System.out.println("=====这是一个抽象方法的实现====");
        System.out.println("技能:唐诗");
    }
}

3、测试类

public class MyTest {
    public static void main(String[] args) {
        
        IWriter writer = new TangWriter();
        // 调用抽象方法的实现
        writer.skill();

        // 调用默认方法
        writer.write();

        // 调用静态方法
        writer.tag();
    }
}

try-with-resources 优化

在Java 7 中引入了try-with-resources功能,保证了每个声明了的资源在语句结束的时候都会被关闭。

任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。

在Java 7中需要这样写:

try (BufferedInputStream bufferedInputStream = new BufferedInputStream(System.in);
     BufferedInputStream bufferedInputStream1 = new BufferedInputStream(System.in)) {
    // do something
} catch (IOException e) {
    e.printStackTrace();
}

而到了Java 9无需为 try-with-resource 临时声明变量,简化为:

BufferedInputStream bufferedInputStream = new BufferedInputStream(System.in);
BufferedInputStream bufferedInputStream1 = new BufferedInputStream(System.in);
//变量 定义在try外边,在 try 中包裹,也会自动释放
try (bufferedInputStream;
     bufferedInputStream1) {
    // do something
} catch (IOException e) {
    e.printStackTrace();
}

不支持下划线(_)作为标识符

在早期版本的 Java 中,下划线(_)已用作标识符或创建 变量名称。从 Java 9 开始,下划线字符是一个保留关键字,不能用作标识符或变量名。如果我们使用单个下划线作为标识符,程序将无法编译并抛出编译时错误,因为现在它是一个 关键字,并且在 Java 9 或更高版本中不能用作变量名称。

以下代码java9之前(比如java8)是可以的,java9及其后续版本,编译不通过

int _ = 50;
// 解决办法,多加一个下划线,int __ = 50;

允许匿名类使用 <>(省略泛型的标记)

如果推断类型的参数类型是可忽略的,则允许使用匿名类的菱形。

在 Java 7 中引入的钻石操作符简化了泛型实例的创建,但它不能用于匿名内部类。由于这个限制,开发者不得不在使用匿名内部类时指定泛型参数,这增加了代码的冗余和复杂性。

在 Java 9 中,钻石操作符得到了改进,允许与匿名内部类配合使用。现在,当我们实例化一个具有泛型参数的匿名内部类时,无需显式指定这些参数,因为 Java 编译器能够利用上下文信息自动推导出正确的类型。

下面的代码在 Java 8 及以下版本是无法编译通过的,在 Java 9 及以上版本中可以:

public class MyTest {
  class InnerC<T> {}
	@Test
    public void testJep213() {
        InnerC<String> stringValue = new InnerC<>() {
        }; // 这里java8会报错
    }
}

4、Stream API 增强

Stream API 是在Java 8 中闪亮登场的,Java 9 对 Stream API 做了一些增强。

ofNullable

Stream ofNullable(T t) 返回包含单个元素的顺序Stream ,如果非空,否则返回空Stream 。

这就意味着我们可以使用 Stream.ofNullable() 方法来快速创建一个只包含非空元素的 Stream,且无需担心处理 null 值的边界情况。比如,我们有一个 List 包含一些可能为 null 的字符串,我们可以使用 Stream.ofNullable() 来创建一个只包含非空字符串的 Stream:

public static void main(String[] args) {
    Stream<String> stream1 = Stream.ofNullable(null);
    System.out.println("Stream记录:");
    stream1.forEach(System.out::println);

    Stream<List<String>> stream2 = Stream.ofNullable(List.of("大学", "中庸", "论语", "孟子"));

    System.out.println("Stream记录:");
    stream2.forEach(System.out::println);
    
     // 有null值,flatMap将集合扁平化处理
    List<String> list = Arrays.asList("大学", null, "中庸", "论语", null, "孟子");
    list.stream().flatMap(Stream::ofNullable).forEach(System.out::println);
}

iterate

Java 8 中的 iterate() 用于创建一个无限流,其元素由给定的初始值和一个生成下一个元素的函数产生,为了终止流我们需要使用一些限制性的函数来操作,例如 limit():

Stream.iterate(1,v -> v * 2).limit(10).forEach(System.out::println);

在 Java 9 中为了限制流的长度,增加了断言谓词,方法定义如下:

Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)

这个是用来生成有限流的新迭代实现。

  • seed 初始种子值
  • hasNext 用来判断何时结束流,如果 hasNext 返回 true,则next 函数就会继续生成下一个元素;一旦 hasNext 返回 false,序列生成将停止。
  • next函数用来计算下一个元素值。

示例:

//大约等于5时停止计算,返回结果 0 1 2 3 4
Stream.iterate(0, i -> i < 5, i -> i + 1).forEach(System.out::println);

等同于传统的:

for (int i = 0; i < 5; ++i) {
    System.out.println(i);
}

takeWhile

Stream.takeWhile(Predicate) 从流的开头开始,选择满足断言Predicate条件的所有元素,直到找到第一个不满足条件的元素,一旦找到该元素,将会终止选择元素(即使未断言中的元素有满足条件的),并返回一个新的流,其中包含了前缀元素。

// 输出结果 2, 4, 6, 8。遇到10不满足条件终止,即使后面有2这个符合条件的元素也不再处理。
Stream.of(2, 4, 6, 8, 10, 2).takeWhile(x -> 1 < x && x < 10).forEach(System.out::println);

// 返回结果为空。这里如果将1改为3,那么返回结果为空,因为第一个元素就不满足条件,后面的直接跳过
Stream.of(2, 4, 6, 8, 10, 2).takeWhile(x -> 3 < x && x < 10).forEach(System.out::println);

// 输出结果 2, 4。遇到6不满足条件终止,即使后面有2这个符合条件的元素也不再处理。
Stream.of(2, 4, 6, 8, 10, 2).takeWhile(x -> 1 < x && x < 6).forEach(System.out::println);

dropWhile

这个API和takeWhile机制类似,也用来筛选Stream中的元素。

  • 方法会从流的开头开始,跳过满足断言条件的所有元素,直到找到第一个不满足条件的元素。一旦找到第一个不满足条件的元素,dropWhile() 将停止跳过元素,并返回一个新的流,其中包含了剩余的元素。
  • 如果流的所有元素都满足谓词条件,那么返回一个空流。
// 输出结果 10 3。大于1小于10的被跳过,知道遇到第一个不满足条件的10,后面的直接返回不再处理。 
Stream.of(2, 4, 6, 8, 10, 3).dropWhile(x -> 1 < x && x < 10).forEach(System.out::println);

5、Optional 的增强

Optional增加了三个有用的API。

  • stream() Optional现在可以转Stream。
  • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) 如果有值了怎么消费,没有值了怎么消费。
  • or(Supplier<? extends Optional<? extends T>> supplier) 该方法接受一个 Supplier作为参数,如果当前Optional有值就返回当前Optional,当前Optional 为空时,将执行Supplier获取一个新的Optional 对象。可以用于提供默认值。

ifPresentOrElse

Java 8 有一个 ifPresent(),它用于在 Optional 包含非空值时执行指定的操作,Java 9 对其进行了改进增加了一个else处理。

public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)

它有两个参数:

  1. action:是一个 Consumer 函数式接口,用于在 Optional 包含非空值时执行的操作。
  2. emptyAction:是一个 Runnable 函数式接口,用于在 Optional 为空时执行的操作,通常是一些默认操作或错误处理。
private void testJava9(User user1) {
    System.err.println("=====java 9=====");
    Optional.ofNullable(user1).ifPresentOrElse(u -> {
        System.err.printf("姓名%s 年龄%s %n", user1.getName(), user1.getAge());
    }, () -> {
        System.err.println("user 对象为null");
    });
}

private void testJava8(User user1) {
    System.err.println("=====java 8=====");
    Optional.ofNullable(user1).ifPresent(u -> {
        System.err.printf("姓名%s 年龄%s %n", user1.getName(), user1.getAge());
    });
}

private void testCommon(User user1) {
    System.err.println("=====直接判断=====");
    if (user1 != null) {
        System.err.printf("姓名%s 年龄%s %n", user1.getName(), user1.getAge());
    } else {
        System.err.println("user 对象为null");
    }
}

@Test
public void testOptionalIfPresent() {
    User user1 = null;
    testCommon(user1);

    testJava8(user1);

    testJava9(user1);

    System.err.println();
    System.err.println();
    System.err.println("*****赋值之后*****");
    user1 = User.builder().name("kevin").build();
    testCommon(user1);

    testJava8(user1);

    testJava9(user1);
}

结果

=====直接判断=====
user 对象为null
=====java 8=====
=====java 9=====
user 对象为null

*****赋值之后*****
=====直接判断=====
姓名kevin 年龄null 
=====java 8=====
姓名kevin 年龄null 
=====java 9=====
姓名kevin 年龄null 

or

public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)

Optional.or()接受一个 Supplier作为参数,如果当前Optional有值就返回当前Optional,当前Optional 为空时,将执行Supplier获取一个新的Optional 对象。可以用于提供默认值。

@Test
public void testOptionalOr() {
    // 创建一个 optional
    Optional<String> optional = Optional.of("曹植");
    // 创建一个 Supplier(生产者)
    Supplier<Optional<String>> supplierString = () -> Optional.of("未设置");
    // 输出 作者: 曹植
    System.out.println("作者: " + optional.or(supplierString).get());

    // option置空后。输出的是:作者: 未设置
    optional = Optional.empty();
    System.out.println("作者: " + optional.or(supplierString).get());
}

stream

public Stream<T> stream()

stream() 方法使得 Optional 对象能够被转换成一个流(Stream)。这样就可以利用 Stream 的各种功能来处理 Optional 中的值。

@Test
public void testOptionalStream() {
    List<Optional<String>> list = List.of(
        Optional.of("曹操"), 
        Optional.of("曹植"), 
        Optional.empty(), 
        Optional.of("曹丕"), 
        Optional.empty());
    
    list.stream().flatMap(Optional::stream).filter(o -> o.equals("曹植")).map(val -> val + " —— 建安七子之一").forEach(System.out::println);
}

6、JEP 110:HTTP2客户端

JEP 110

定义一个新的 HTTP 客户端 API 来实现 HTTP/2 和 WebSocket,并且可以替换旧的HttpURLConnectionAPI。Java以前原生的确实难用,所以诞生了Apache Http Components 、OkHttp等好用的客户端。

@Test
public void testHttpRequest() {
    try {
        URI uri = new URI("https://www.baidu.com");
        HttpRequest httpRequest = HttpRequest.newBuilder(uri).header("Content-Type", "*/*").GET().build();
        HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .version(HttpClient.Version.HTTP_2).build();
        HttpResponse<String> response = httpClient.send(httpRequest, BodyHandlers.ofString());
        int statusCode = response.statusCode();
        String body = response.body();

        System.out.println(statusCode);
        System.out.println(body);

    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

}

7、JEP 266:CompletableFuture增强

JEP 266

CompletableFuture 是 Java 8 中引入用于处理异步编程的核心类,它引入了一种基于 Future 的编程模型,允许我们以更加直观的方式执行异步操作,并处理它们的结果或异常。

但是在实际使用过程中,发现 CompletableFuture 还有一些改进空间,所以 Java 9 对它做了一些增强,主要内容包括:

  • 新的工厂方法

  • 支持延迟执行和超时(timeout)机制

  • 支持子类化

支持超时机制

如果执行超时, orTimeout() 方法直接抛出了一个异常,而 completeOnTimeout() 方法则是返回默认值

// 允许为 CompletableFuture 设置一个超时时间。如果在指定的超时时间内未完成,将抛出 TimeoutException 异常
public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)
// 允许为 CompletableFuture 设置一个超时时间。如果在指定的超时时间内未完成,则返回默认值value
public CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit) 

示例:

@Test
public void testTimeout() {
    // 如果执行超时, orTimeout() 方法直接抛出了一个异常,而 completeOnTimeout() 方法则是返回默认值
    try {
        // orTimeout() 超时抛出了一个异常java.util.concurrent.TimeoutException
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(this::longLongAfter).orTimeout(1, TimeUnit.SECONDS);

        future.get(); // 显式等待超时
    } catch (Exception e) {
        System.out.println(e);
    }

    try {
        int defaultValue = 1987;
        // completeOnTimeout() 超时返回默认值
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(this::longLongAfter).completeOnTimeout(defaultValue, 1,
                                                                                                                 TimeUnit.SECONDS);

        Integer result = future.get(); // 显式等待超时
        System.out.println(result);
    } catch (Exception e) {
        System.out.println(e);
    }

}
// 这是一个超时方法
private Integer longLongAfter() {
    try {
        Thread.sleep(Integer.MAX_VALUE); // 一直睡,就是为了超时
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return 666;
}

执行结果:

java.util.concurrent.ExecutionException: java.util.concurrent.TimeoutException
1987

支持延迟执行

CompletableFuture 类通过 delayedExecutor() 方法提供了对延迟执行的支持,该方法负责生成一个具有延后执行功能的 Executor,使得任务可以在未来指定的时间点才开始执行。

public static Executor delayedExecutor(long delay, TimeUnit unit)

示例:

@Test
public void testDelay() {
    System.out.println(LocalDateTime.now());
    // 创建一个延迟执行的Executor.5秒后执行
    Executor delayedExecutor = CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS);

    // 使用延迟的Executor执行一个简单任务
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
        System.out.println(LocalDateTime.now());
        System.out.println("任务延迟后执行...");
    }, delayedExecutor);

    // 等待异步任务完成
    future.join();
}

执行结果:

2024-02-20T16:07:00.314069600
2024-02-20T16:07:05.322898

可以看到相隔5秒。

8、JEP 261:模块化系统

JEP 261

设计理念非常好,但目前对于开发人员来说比较鸡肋。

模块化系统是 Java9 架构的一次重大变革,它旨在解决长期以来 Java 应用所面临的一些结构性问题,特别是在大型系统和微服务架构中。

如果把 Java 8 比作单体应用,那么引入模块系统之后,从 Java 9 开始,Java 就华丽的转身为微服务。模块系统,项目代号 Jigsaw,最早于 2008 年 8 月提出,2014 年跟随 Java 9 正式进入开发阶段,最终跟随 Java 9 发布于 2017 年 9 月。

那么什么是模块系统?官方的定义是A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor。模块的载体是 jar 文件,一个模块就是一个 jar 文件,但相比于传统的 jar 文件,模块的根目录下多了一个 module-info.class 文件,也即 module descriptor。 module descriptor 包含以下信息:

  • 模块名称
  • 依赖哪些模块
  • 导出模块内的哪些包(允许直接 import 使用)
  • 开放模块内的哪些包(允许通过 Java 反射访问)
  • 提供哪些服务
  • 依赖哪些服务

我们知道在 Java 中, Java 文件是最小的可执行文件,为了更好地管理这些 Java 文件,我们需要用 package 将同一类的 Java 文件统一管理起来,多个 package 文件、Java 文件可以打包成一个 jar 文件,现在 Java 9 在 package 上面增加 module,一个 module 可以包含多个 package,所以从代码结构上来看层级关系是这样的:jar > module > package > java 文件。

要掌握模块化,就需要理解它的几个核心概念:

  1. 模块(Module):模块是模块化系统的基本单元。它是一个逻辑上独立的代码单元,包括类、接口、资源和module-info.java文件。每个模块都有一个唯一的名称,例如:"java.base"、"com.example.myapp"等。
  2. 模块路径(Module Path):模块路径是一组包含模块的路径,用于在运行时指定应用程序所需的模块。类似于类路径,但它是用于模块。
  3. module-info.java 文件:每个模块都包含一个特殊的文件,名为module-info.java。这个文件描述了模块的信息,包括模块名称、依赖关系、导出的包以及其他模块信息。
  4. 模块依赖性(Module Dependencies):在module-info.java文件中,可以使用requires关键字声明模块之间的依赖关系。
  5. 模块导出(Module Exporting):在module-info.java文件中,可以使用 exports 关键字声明哪些包可以被其他模块访问,这有助于控制包的可见性。

为什么需要模块化

引入模块化其实在一定程度上增加编码的复杂度,特别是对于之前习惯于传统的包结构的开发者来说。然而,引入模块化有着一些非常重要的优势,这些优势可以帮助我们更好地组织、管理和维护我们的Java应用程序,提高开发效率和代码质量。

  • 更好的代码组织:模块化允许开发者将代码划分为独立的模块,每个模块都可以作为一个功能单元进行开发和测试。这有助于编写高内聚低耦合的代码,并清晰地管理模块间的依赖关系。
  • 提升性能与可伸缩性:通过模块化,JDK 和 JRE 可以重新安排到可互操作的模块中,支持创建可在小型设备上执行的可扩展运行时。模块化还使得 Java 应用更容易适配到更小的设备中,这对于嵌入式系统和物联网设备尤为重要。
  • 改善安全和维护性:模块化 JDK 和 JRE 可以提高安全性、维护性,并允许定制运行时环境。例如,如果某个网络应用不需要 Swing 图形库,可以在打包应用时选择不包含该库,从而减少性能消耗。
  • 提高编译效率:在 Java 9 中,构建系统通过 JEP 201 进行编译和实施模块边界,增强了在构建时编译模块和识别模块边界的能力。
  • 促进大型项目管理:对于大型和复杂的应用程序,模块化可以将应用分解为完成特定功能的小块,这有助于简化开发过程和管理依赖关系。
  • 支持运行时组合:模块化允许在运行时动态组合不同的模块,提供了更大的灵活性和可定制性。
  • 便于迁移和维护:模块化可以帮助开发者逐步迁移旧的非模块化代码库至新的模块化结构,而不必一开始就全面转换整个代码库。

模块化怎么体现的呢?下图是Java 8与Java 9的目录结构:

java8目录

java9目录

可以看出 Java 9 中没有jre,没有rt.jar,没有tools.jar,而是多了一个 jmods,该文件夹下都是一个一个的模块:

添加图片注释,不超过 140 字(可选)

在Java 9之前的项目中,一个简单的"hello world"程序,也需要引入rt.jar文件,导致生成的jar包比较庞大。然而,Java 9引入了模块化的概念,使得情况有了改变。现在,使用Java 9及更高版本,你只需要引入程序所依赖的模块,而不是整个rt.jar,这使得构建简单程序时所生成的jar包大小大大减小。这种模块化的方式使得Java应用程序更加轻量化、灵活,也更容易管理和维护。

使用模块化

创建项目

创建一个空白的maven项目,里面只定义一个类

添加图片注释,不超过 140 字(可选)

package com.ld.mytest.jmods;

public class Main {
    public static void main(String[] args) {
        System.out.println("唐宋元明清");
    }
}

创建module-info.java

在根目录下创建module-info.java类

module kevin.test { // 模块名
    requires java.base; // 依赖的模块

    exports com.ld.mytest.jmods; // 导出的模块
}

编译 Java 项目

在class路径下可以看到module-info.class文件。

创建 jmod 文件

使用 jmod create 命令:

jmod create --class-path . kevin.test.jmod
# 命令格式:
jmod create --class-path [module-info.class文件对应的路径] [输出的jmod文件名]

添加图片注释,不超过 140 字(可选)

启动可执行模块

上面咱们创建的模块中是有 Main 入口的可执行模块, 那么能不能像 java -jar 一样执行这个模块呢?

答案是肯定的:

使用 java --module 命令:

java --module-path . --module kevin.test/com.ld.mytest.jmods.Main
# 命令格式:
java --module-path [模块文件所在路径] --module [模块名称]/[包名.main类名]

我这里使用java11运行演示效果,可以看到打印了文本信息。

添加图片注释,不超过 140 字(可选)

相信大家已经意识到了,要想充分发挥模块化的优势,整个 Java 生态系统中的开发者必须遵循规范,将各自的 JAR 文件模块化。此外,在日常的开发实践中,确切地定义自己的模块以及妥善管理模块间的依赖关系,本身就是一项颇具挑战性的任务。

那么我们常用的 Spring 有没有被模块化打动,也按规范进行模块化了呢?至少到 Spring5 还没有,但是这里有一些答案:

1:Declare Spring modules with JDK 9 module metadata

SpringFramework 官方的回答: github.com/spring-proj… 机器翻译:JDK 9 的 Jigsaw 计划旨在允许将模块元数据 (module-info.java) 添加到框架和库 jar 中,同时保持它们与 JDK 8 的兼容性。让我们对 Spring Framework 5.0 的模块尽可能地这样做。然而,我们可能无法以这种方式表达我们的可选依赖安排,在这种情况下,我们可能不得不采用 “自动模块” 方法来实现 #18289 中更温和的目的。

2:Any plans for Java 9 Jigsaw (module) of Spring projects?

stackoverflow.com/questions/4…

9、JEP 102:Process API

JEP 102

在 Java 9 之前,Java 在进程控制方面的功能较为匮乏,获取系统进程的详尽信息及管理这些进程具有一定的难度。这迫使开发者不得不依赖特定于平台的解决方案来执行这些操作,从而影响了代码的跨平台兼容性和便捷性。

为了提升在操作系统级别管理和控制进程的便捷性和效率,Java 9 推出了新的 Process API。这一举措旨在增强对操作系统进程的控制和管理,同时确保跨不同操作系统的行为一致性。新 API 的主要功能如下:

  • 增强的 Process 类:Java 9 增强了 Process 类,提供了更多方法来管理和控制进程。
  • ProcessHandle 接口:引入了 ProcessHandle 接口,它提供了获取进程的 PID(进程标识符)、父进程、子进程、进程状态等信息的能力。
  • 流式 API:利用流式 API,可以更方便地处理进程的信息和状态。

下面是获取本地所有进程的相关信息:

@Test(priority = 1) // 不指定顺序时默认按字母顺序执行
public void testStream() {
    ProcessHandle.allProcesses() // 获取所有进程
        .forEach(processHandle -> {
            System.out.printf("进程ID: %s, 命令: %s, 启动时间: %s, 用户: %s%n", //
                              processHandle.pid(), // 获取进程ID
                              processHandle.info().command().orElse("未知"), // 获取进程的命令信息
                              processHandle.info().startInstant().map(i -> i.toString()).orElse("未知"), // 获取进程的启动时间
                              processHandle.info().user().orElse("未知")); // 获取运行进程的用户
        });
}

执行结果(部分):

进程ID: 2796, 命令: C:\Program Files (x86)\Notepad++\notepad++.exe, 启动时间: 2024-02-20T00:46:59.797Z, 用户: lk-202304051138\LD_001
进程ID: 18516, 命令: C:\Program Files\Typora\Typora.exe, 启动时间: 2024-02-20T00:52:34.161Z, 用户: lk-202304051138\LD_001
进程ID: 22920, 命令: C:\Windows\explorer.exe, 启动时间: 2024-02-20T01:02:48.523Z, 用户: lk-202304051138\LD_001
进程ID: 24468, 命令: C:\Program Files\Mozilla Firefox\firefox.exe, 启动时间: 2024-02-20T01:50:04.012Z, 用户: lk-202304051138\LD_001
进程ID: 24972, 命令: C:\Windows\ImmersiveControlPanel\SystemSettings.exe, 启动时间: 2024-02-20T01:57:29.345Z, 用户: lk-202304051138\LD_001
进程ID: 18072, 命令: C:\Program Files\WindowsApps\Microsoft.Windows.Photos_2023.10030.27002.0_x64__8wekyb3d8bbwe\Microsoft.Photos.exe, 启动时间: 2024-02-20T02:07:29.693Z, 用户: lk-202304051138\LD_001
进程ID: 1452, 命令: C:\Windows\System32\RuntimeBroker.exe, 启动时间: 2024-02-20T02:07:42.743Z, 用户: lk-202304051138\LD_001
进程ID: 22752, 命令: C:\Program Files (x86)\WXWork\4.1.20.6015\updated_web\WXWorkWeb.exe, 启动时间: 2024-02-20T03:11:06.533Z, 用户: lk-202304051138\LD_001
进程ID: 29060, 命令: C:\Program Files (x86)\Tencent\WeChat\WeChat.exe, 启动时间: 2024-02-20T04:07:25.283Z, 用户: lk-202304051138\LD_001
进程ID: 29372, 命令: C:\Program Files (x86)\Tencent\WeChat\WeChatWeb.exe, 启动时间: 2024-02-20T04:07:27.310Z, 用户: lk-202304051138\LD_001
进程ID: 30340, 命令: C:\Program Files (x86)\Tencent\WeChat\WeChatApp.exe, 启动时间: 2024-02-20T04:08:03.707Z, 用户: lk-202304051138\LD_001
进程ID: 28952, 命令: 未知, 启动时间: 未知, 用户: 未知
进程ID: 18232, 命令: C:\Program Files (x86)\Tencent\QQPCMgr\13.10.21936.216\QMUsbGuard.exe, 启动时间: 2024-02-20T04:31:27.884Z, 用户: lk-202304051138\LD_001
进程ID: 14000, 命令: C:\Users\LD_001\AppData\Local\SogouExplorer\SogouExplorer.exe, 启动时间: 2024-02-20T04:44:55.455Z, 用户: lk-202304051138\LD_001
进程ID: 28584, 命令: F:\eclipse-2023-12\eclipse.exe, 启动时间: 2024-02-20T05:40:41.543Z, 用户: lk-202304051138\LD_001
进程ID: 30864, 命令: 未知, 启动时间: 未知, 用户: 未知
进程ID: 33500, 命令: 未知, 启动时间: 未知, 用户: 未知
进程ID: 31136, 命令: F:\Program Files\Java\jdk-11.0.12\bin\javaw.exe, 启动时间: 2024-02-20T05:46:38.871Z, 用户: lk-202304051138\LD_001

10、JEP 259:Stack-Walking API

JEP 259

提供一个堆栈遍历API,允许轻松筛选和延迟访问堆栈跟踪中的信息。

API既支持在符合给定条件的处停止的搜索,也支持遍历整个堆栈。可参阅类java.lang.Stackwalker

@Test
public void testJep213() {
    System.out.println(StackWalker.getInstance().walk(s -> s.limit(5).collect(Collectors.toList())));
    System.out.println(StackWalker.getInstance().walk(s -> s.collect(Collectors.toList())));
}

11、JEP 264:平台日志 API 和 服务

JEP 264

12、JEP 266:响应式流(Reactive Streams)

JEP 266

www.reactive-streams.org/

Spring WebFlux响应式Web框架已经4年了,响应式流规范(reactive streams)在Java 9 中也初步引入到了JDK中。

在 Java 9 之前,没有一种标准方式来在 Java 中实现响应式编程。因此,Java 9 推出了响应式流规范,旨在为处理 Java 中的异步数据流提供标准化方法。确保了高效的运作和低延迟的响应时间,这使得接收方能够控制数据流动的节奏,从而避免因数据生产过快而导致的溢出问题。

主要内容为:

引入了java.util.concurrent.Flow类,它包含了几个嵌套的静态接口:Publisher、Subscriber、Subscription、Processor

  • Publisher:一个数据流的生产者。
  • Subscriber:订阅 Publisher 并处理数据的消费者。
  • Subscription:连接 Publisher 和 Subscriber,允许 Subscriber 控制数据流。
  • Processor:充当生产者和消费者的中间人,即 Publisher 和 Subscriber 的组合。

13、JEP 277:增强的@Deprecated注解

JEP 277

@Deprecated 注解用于标记过时的 API,Java 9 对其进行了改进,增加了两个的属性:

  • since :指明从哪个版本开始 API 被弃用。

  • forRemoval:指出这个 API 是否计划在未来的版本中被移除。

14、JEP 224:HTML5 Javadoc

JEP 224

在 Java 9 以前,Javadoc 主要遵循的是老旧的 HTML 4 标准。Java 9 推出了对 HTML5 的支持,将 Javadoc 文档的标准升级至 HTML5。这一举措显著提升了文档的兼容性、用户体验和可访问性,它不仅优化了对现代浏览器特性的支持,还改进了布局与样式,同时增强了搜索功能的效率。

15、JEP 238:多版本兼容 JAR 文件

JEP 238

在 Java 9 之前,一个 JAR 文件只能包含针对一个特定 Java 版本编译的类文件。随着 Java 平台的持续进化和新版本的频繁发布,这种限制不利于jar包在不同 Java 版本的兼容。

Java 9 扩展JAR文件格式,使多个特定于Java发行版的类文件版本能够共存于一个归档文件中。 使得库开发者可以在单个 JAR 文件中包含针对不同 Java 版本编译的类文件。这样,应用程序可以在不同的 Java 运行时环境中运行,而无需更改或重新打包。