Java17 隐藏宝藏,从 Stream.mapMulti 到 HexFormat
一、前言
除了众所周知的 JEP 之外,Java 17 还有更多内容。首先请确认 java 版本:
$ java --version
openjdk 17 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
下面跟随我一起来看看这些宝藏。
二、新功能 (JDK 16): java.time 格式化添加一天中的时间段
有时候我们希望表示一天中的时段,例如:“上午”、“下午” 或 “晚上”,而不仅仅是上午或下午。为了解决这个问题,有一个新的时间格式化模式,称为 B,它已添加到 java.time.format.DateTimeFormatter 和DateTimeFormatterBuilder 类中,并且已经内置国际化支持。示例:
String format = DateTimeFormatter.ofPattern("B").format(LocalTime.now());
System.out.println(format); // 上午
三、新功能 (JDK 16): 新增 Stream.toList() 方法
自从 Java8 中引入 Stream API 以来,它的冗长性一直倍受吐槽。例如:在字符串列表和数字列表上执行简单的映射转换需要写入尽可能多的代码:
List<String> numbers = List.of("1","2","3","4","5");
numbers.stream().map(Integer::valueOf).collect(Collectors.toList());
使用新的 toList()
方法,可以使你的代码更短、更清晰,例如:
numbers.stream().map(Integer::valueOf).toList();
下面有2点你可能需要注意:
Stream.toList()
生成的是一个 unmodifiable List,同时它 不接受 null,会抛出空指针异常。Stream.toList()
不是collect(toUnmodifiableList())
的简写,它不受Collector
接口约束;这样Stream.toList()
消耗的内存更少。在预先知道流大小时的场景它是最佳选择。
四、新功能 (JDK 16): 新增 Stream.mapMulti() 方法
mapMulti()
方法签名如下:
default <R> Stream<R> mapMulti(BiConsumer<? super T,? super Consumer<R>> mapper)
如你所见,它是一个中间操作方法,以 default 方法添加到 Stream
接口中。其他版本包括针对 int
、long
和 double
的特定类型的方法:mapMultiToInt()
、mapMultiToLong()
和 mapMultiToDouble()
。
这些操作返回一个流,其中该流的每个元素都替换为零个或多个元素。
替换是通过将提供的映射函数应用于每个元素以及接受替换元素的消费者参数来执行的。映射函数调用消费者零次或多次以提供替换元素。
让我们看看在下面三种场景中如何使用 mapMulti()
。
4.1 第一个场景: Zero-to-one (0...1) 映射
对一些选定的项目使用mapMulti() mapper.accept(R r)
(consumer)可以实现类似 filter 的 pipeline。例如:使用它可能有助于根据谓词检查元素,然后将其映射到不同的值。
在下面的代码片段中,您只想过滤长度大于或等于五个字符的“Java”及其项目名称,然后您想将每个名称替换为其等效长度:
Stream.of("Java", "Valhalla", "Panama", "Loom", "Amber")
.mapMulti((str, mapper) -> {
if (str.length() >= 5)
mapper.accept(str.length()); // 长度大于等于 5
})
.forEach(i -> System.out.print(i + " ")); // 8 6 5
在没有 mapMulti()
方法时我们则需要使用 filter
和 map
的组合来实现同样的效果。
4.2 第二个场景: One-to-one (1...1) 映射
再次使用上面的示例,如果省略条件并且流中的每个元素被映射到一个新元素并使用映射器接受,将得到以下结果。在这个例子中,mapMulti()
的行为就像一个 map()
。
Stream.of("Java", "Valhalla", "Panama", "Loom", "Amber")
.mapMulti((str, mapper) -> mapper.accept(str.length()))
.forEach(i -> System.out.print(i + " ")); // 4 8 6 4 5
3 第三个场景: One-to-many (1...*) 映射
如上所述,mapper.accept(R r)
(consumer)可以被调用任意次数。让我们修改代码片段,用名称的长度替换每个项目名称的字符。例如,“Java”变为“4444”,“Panama”变为“666666”,空字符串变为空。
Stream.of("Java", "Valhalla", "Panama", "Loom", "Amber", "")
.mapMulti((str, mapper) -> {
for (int i = 0; i < str.length(); i++) {
mapper.accept(str.length());
}
mapper.accept(" ");
})
.forEach(System.out::print) // 4444 88888888 666666 4444 55555
什么场景下使用 mapMulti() 而不是 flatMap()? mapMulti
的主要思想是它的映射器可以被(零次和)多次调用。此外,mapMulti 方法使用的SpinedBuffer
内部允许将元素推送到单个扁平化的 Stream
实例中,而不需要为每组输出元素创建一个新实例。这是与 flatMap
的一个关键区别。
正如 API 文档所指出的,mapMulti
在下面 2 个场景中,它比使用 flatMap
更适合:
用少量(可能为零)元素替换每个流元素。使用这种方法可以避免为每组结果元素创建一个新的 Stream
实例,正如flatMap
所要求的那样。当使用命令式方法生成结果元素比以 Stream 的形式返回它们更容易时。
文档补充说明:因为它一次只创建一个 Stream
,“在性能方面,mapMulti
在上述场景中是赢家。”
五、Bug 修复 (JDK 16): Path.of 或 Paths.get 第一个参数为 null 时应抛出空指针异常
Path.of()
和 Paths.get()
方法参数 JDK 16 版本中已更改为在第一个参数为 null 时一致地抛出 NullPointerException
,如下所示:
Path path = Path.of(null,"path/to/file");
| Exception java.lang.NullPointerException
| at Objects.requireNonNull (Objects.java:208)
| at UnixFileSystem.getPath (UnixFileSystem.java:263)
| at Path.of (Path.java:147)
| at (#21:1)
在老版本中,这些方法在使用多个参数调用时会错过对第一个参数的空检查,因此如果代码在 JDK 11 下运行,会得到如下结果:
Path path = Path.of(null,"path/to/file") // null/path/to/file
六、新功能 (JDK 17): 新增 java.util.HexFormat
新的 HexFormat 可用于在 bytes 和 chars 以及 hex 字符串之间转换,可添加额外的格式标记,如 prefixes(前缀)、suffixes(后缀) 和 delimiters(分隔符)。HexFormat
是基于值的类 请注意,HexFormat 对象应该使用 equals
方法进行比较。另外 HexFormat
是不可变的和线程安全的。
// 1. 构造 HexFormat 对象,默认:uppercase: false, delimiter: "", prefix: "", suffix: ""
HexFormat hex = HexFormat.of();
// 2. byte 转 hex 字符串
byte b = 127;
String byteStr = hex.toHexDigits(b); // 7f
// 3. hex 字符串转数回 byte
byte digits = (byte) HexFormat.fromHexDigits(byteStr); // 127
// 自定义 HexFormat
HexFormat format = HexFormat.of()
.withPrefix("mica") // 前缀
.withDelimiter(" ") // 分隔符
.withSuffix("微服务") // 后缀
.withUpperCase(); // hex 使用大写
// hex 编码
String hexStr = format.formatHex("123".getBytes());
System.out.println(hexStr); // mica31微服务 mica32微服务 mica33微服务
// hex 解码
byte[] hexBytes = format.parseHex(hexStr);
System.out.println(new String(hexBytes)); // 123
七、总结
我们已经陆续发了多篇 Java17 相关文章,还有一批文章正在酝酿。
另外笔者开源的微服务组件 mica 已经适配 java17。关注 JAVA架构日记,学习技术不迷路!