Lambda 表达式是 Java SE 8 的核心功能,大部分的改进都围绕 lambda 表达式展开。( Jigsaw 项目已经被推迟到 Java SE 9。)关于 lambda 表达式的内容,已经在上一篇文章中进行了说明。这篇文章主要介绍 Java SE 8 中包含的其他 Java 标准库的增强。
并行排序
随着多核 CPU 的流行,Java 平台的标准库实现也尽可能利用底层硬件平台的能力来提高性能。Java SE 7 中引入了 Fork/Join 框架作为一个轻量级的并行任务执行引擎。Java SE 8 把 Fork/Join 框架用到了标准库的一些方法的实现中。比较典型的是 java.utils.Arrays 类中新增的 parallelSort 方法。与已有的 sort 方法不同的是,parallelSort 方法使用 Fork/Join 框架来实现。在多核 CPU 平台上的性能更好。下面的代码对包含 1 亿个整数的数组分别使用 parallelSort 和 sort 进行排序。
Random random = new Random(); int count = 100000000; int[] array = new int[count]; Arrays.parallelSetAll(array, (index) -> random.nextInt()); int[] copy = new int[count]; System.arraycopy(array, 0, copy, 0, array.length); Arrays.parallelSort(array); Arrays.sort(copy);
在本人的 4 核 CPU 的平台上,parallelSort 和 sort 方法的耗时分别是 7112 毫秒和 16777 毫秒。所以 parallelSort 方法的性能要好不少。不过 parallelSort 方法只在数据量较大时有比较明显的性能提升。当数据量较小时,Fork/Join 框架本身所带来的额外开销足以抵消它带来的性能提升。
集合批量数据操作
在 Java 应用的开发中,对集合的操作是比较常见的。不过在 Java SE 8 之前的 Java 标准库中,对集合所能进行的操作比较有限,基本上都围绕集合遍历来展开。相对于其他编程语言来说,Java 标准库在这一块是比较弱的。Java SE 8 中 lambda 表达式的引入以及标准库的增强改进了这种状况。具体来说体现在两个方面上的改进:第一个方面是对集合的操作方式上。得益于默认方法的引入,Java 集合框架中的接口可以进行更新,添加了更多有用的操作方式,即通常所说的“filter/map/reduce”等操作。第二个方面是对集合的操作逻辑的表示方式上。新添加的操作方式使用了 java.util.function 包中的新的函数式接口,可以很方便地使用 lambda 表达式来表示对集合的处理逻辑。这两个方面结合起来,得到的是更加直观和简洁的代码。
新的集合批量处理操作的核心是新增的 java.util.stream 包,其中最重要的是 java.util.stream.Stream 接口。Stream 接口的概念类似于 Java I/O 库中的流,表示的是一个支持顺序和并行操作的元素的序列。在该序列上可以进行不同的转换操作。序列中包含的元素也可以被消费以产生所需的结果。Stream 接口所表示的只是操作层面上的抽象,与底层的数据存储并没有关系。通常的使用方式是从集合中创建出 Stream 接口的对象,再进行各种不同的转换操作,最后消费操作执行的结果。
Stream 接口中包含的操作分成两类:第一类是对序列中元素进行转换的中间操作,如 filter 和 map 等。这类中间操作是延迟进行的,可以级联起来。第二类是消费序列中元素的终止操作,如 forEach 和 count 等。当对一个 Stream 接口的对象执行了终止操作之后,该对象无法被再次处理。这点符合一般意义上对于“流”的理解。下面的代码给出了 Stream 接口中的 filter、map 和 reduce 操作的基本使用方式。Stream 接口中的方法大量使用了函数式接口,可以用 lambda 表达式很方便地进行操作。
IntStream.range(1,10).filter(i -> i % 2 == 0).findFirst().ifPresent(System.out::println); // 保留偶数并输出第一个元素 <p> IntStream.range(1,10).map(i -> i * 2).forEach(System.out::println); // 所有元素乘以 2 并输出 </p>
int value = IntStream.range(1, 10).reduce(0, Integer::sum); // 求和
Stream 接口的 reduce 操作还支持一种更加复杂的用法,如下面的代码所示:
List<String> fruits = Arrays.asList(new String[] {"apple", "orange", "pear"}); int totalLength = fruits.stream().reduce(0, (sum, str) -> sum + str.length(), Integer::sum); // 字符串长度的总和 {1}
这种方式的 reduce 方法需要 3 个参数,分别是初始值、累积函数和组合函数。初始值是 reduce 操作的起始值;累积函数把部分结果和新的元素累积成新的部分结果组合函数则把两个部分结果组合成新的部分结果,最后产生最终结果。这种形式的 reduce 操作通常可以简化成一个 map 操作和另外一个简单的 reduce 操作,如下面的代码所示。两种方式的效果是一样的,不过下面的方式更加容易理解一些。
int totalLength = fruits.stream().mapToInt(String::length).reduce(0, Integer::sum);
另外一种特殊的 reduce 操作是 collect 操作。它与 reduce 的不同之处在于,collect 操作的过程中所进行的是对一个结果对象进行修改操作。这样可以避免不必要的对象创建,提高性能。下面代码中的结果是一个 StringBuilder 类的对象。
StringBuilder upperCase = fruits.stream().collect(StringBuilder::new, (builder, str) -> builder.append(str.substring(0, 1).toUpperCase()), StringBuilder::append); // 字符串首字母大写并连接 {1}
Stream 接口中的操作可以是顺序执行或并行执行的。这是在 Stream 接口的对象创建时所确定的。比如 Collection 接口提供了 stream 和 parallelStream 方法来创建两种不同执行方式的 Stream 接口的对象。这两种不同的方式是可以切换的,通过 Stream 接口的 sequential 和 parallel 方法就可以完成。
日期和时间
Java 标准库中的日期和时间处理 API 一直为开发人员所诟病。大多数开发人员会选择 Joda Time 这样的第三方库来进行替代。 JSR 310 作为 Java SE 8 的一部分,重新定义了新的日期和时间 API,借鉴了已有第三方库中的最佳实践。I 定义在 java.time 包中的新的日期和时间 API 基于标准的 ISO 8601 日历系统。
在新的日期和时间 API 中,核心的类是 LocalDateTime 、 OffsetDateTime 和 ZonedDateTime 。LocalDateTime 类表示的是 ISO 8601 日历系统中不带时区的日期和时间信息。OffsetDateTime 类在基本的日期和时间基础上增加了与 UTC 的偏移量。ZonedDateTime 类则加上了时区的相关信息。下面的代码给出了日期和时间 API 的基本用法,包括对日期和时间的修改、输出和解析。
LocalDateTime.now().plusDays(3).minusHours(1).format(DateTimeFormatter .ISO_LOCAL_DATE_TIME); // 输出日期和时间 ZonedDateTime.now().withZoneSameInstant(ZoneId.of("GMT+08:00")).format (DateTimeFormatter.ISO_ZONED_DATE_TIME); // 输出日期、时间和时区 DateTimeFormatter.ofPattern("yyyy MM dd").parse("200101 25"). query(TemporalQuery.localDate()); // 日期的解析
除了上述 3 个类之外,还有几个值得一提的辅助类。
- Instant :表示时间线上的一个点。当程序中需要记录时间戳时,应该使用该类。Instant 类表示的时间类似“2013-07-22T23:18:35.743Z“。
- Duration :表示精确的基于时间的间隔。比如 Duration.of(30, ChronoUnit.SECONDS) 可以获取 30 秒的间隔。需要注意的是,Duration 类的对象只能从精确的时间间隔创建出来,如秒、小时和天等。在这里,一天表示精确的 24 小时。而月份和年是不能使用的,因为它们不能表示精确的间隔。
- Period :与 Duration 类相对应的 Period 类表示的是基于日期的间隔,只能使用年 / 月 / 日作为单位。Period 类在计算时会考虑夏令时等因素,适合于计算展示给最终用户的内容。
- Clock :表示包含时区信息的时钟,可以获取当前日期和时间。如 LocalDateTime.now(Clock.system(ZoneId.of(“GMT+08:00”))) 表示的是当前的北京时间。Clock 类的一个重要作用是简化测试。在测试时可以指定不同时区的时钟来进行模拟。
其他更新
除了上面提到的几个比较大的更新之前,还有一些小的改动。
Base64 编码
Base64 编码在 Java 应用开发中经常会用到,比如在 HTTP 基本认证中。在 Java SE 8 之前,需要使用第三方库来进行 Base64 编码与解码。Java SE 8 增加了 java.util.Base64 类进行编码和解码。下面的代码给出了简单的示例。
Base64.Encoder encoder = Base64.getEncoder(); String encoded = encoder.encodeToString("username:password".getBytes()); Base64.Decoder decoder = Base64.getDecoder(); String decoded = new String(decoder.decode(encoded));
并发处理
Java SE 8 进一步增强了并发处理的相关 API。在 java.util.concurrent.atomic 中新增了 LongAccumulator 、 LongAdder 、 DoubleAccumulator 和 DoubleAdder 等几个类。这几个类用来在多线程的情况下更新某个 Long 或 Double 类型的变量。下面的代码给出了 LongAccumulator 类的使用示例。
public class ConcurrentSample { public static void main(String[] args) throws Exception { ConcurrentSample sample = new ConcurrentSample(); LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE); for (int i = 0; i < 100; i++) { sample.newThread("Test thread - " + i, accumulator); } System.out.println(accumulator.longValue()); } private void newThread(final String name, final LongAccumulator accumulator) throws Exception { Thread thread = new Thread(() -> { Random random = new Random(); int value = random.nextInt(5000); System.out.println(String.format("%s -> %s", Thread.currentThread ().getName(), value)); accumulator.accumulate(value); }, name); thread.start(); thread.join(); } }
当 LongAccumulator 类的对象上的 accumulate 方法被调用时,参数中的值会通过 Long 类的 max 方法进行比较,所得到的结果作为 LongAccumulator 类的对象的当前值。经过多次累积操作之后,最终的结果是所有调用操作中提供的最大值。
ConcurrentHashMap 类得到了比较大的更新,添加了很多实用的方法,如 compute 方法用来进行值的计算,merge 方法用来进行键值的合并,search 方法用来进行查找等。这使得 ConcurrentHashMap 类可以很方便的创建缓存系统。
感谢张凯峰对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论