Java8 使用(下)
本文主要总结了《Java8实战》,适用于学习 Java8 的同学,也可以作为一个 API 手册文档适用,平时使用时可能由于不熟练,忘记 API 或者语法。
接着 Java8使用(上) 继续补充完剩下的内容。
异步编程
Future
Future 接口在 Java5 中被引入,设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在 Future 中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要呆呆等待耗时的操作完成。
ExecutorService executor = Executors.newCachedThreadPool();
Future < Double > future = executor.submit(new Callable <Double> () {
public Double call() {
return doSomeLongComputation();
}
});
doSomethingElse();
try {
Double result = future.get(1, TimeUnit.SECONDS);
} catch (ExecutionException ee) {
// 计算抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 在Future对象完成之前超过已过期
}
这种编程方式让你的线程可以在 ExecutorService 以并发方式调用另一个线程执行耗时操作的同时,去执行一些其他的任务。
局限性:
Future 接口提供了方法来检测异步计算是否已经结束(使用isDone 方法) ,等待异步操作结束,以及获取计算的结果。
- 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。
- 等待 Future 集合中的所有任务都完成。
- 仅等待 Future 集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值) ,并返回它的结果。
- 通过编程方式完成一个 Future 任务的执行(即以手工设定异步操作结果的方式) 。
- 应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果) 。
CompletableFuture
CompletableFuture 的 completeExceptionally 方法将导致 CompletableFuture 内发生问题的异常抛出。客户端现在会收到一个 ExecutionException 异常,该异常接收了一个包含失败原因的Exception 参数。
使用工厂方法 supplyAsync 创建 CompletableFuture:
public Future <Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync 方法接受一个生产者( Supplier )作为参数,返回一个 CompletableFuture对象, 该对象完成异步执行后会读取调用生产者方法的返回值。 生产者方法会交由 ForkJoinPool池中的某个执行线程( Executor )运行,但是你也可以使用 supplyAsync 方法的重载版本,传递第二个参数指定不同的执行线程执行生产者方法。一般而言,向 CompletableFuture 的工厂方法传递可选参数,指定生产者方法的执行线程是可行的。
CompletableFuture 和 stream 组合使用:
public List <String> findPrices(String product) {
List < CompletableFuture <String>> priceFutures = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() - > shop.getName() + " price is " +
shop.getPrice(product)))
.collect(Collectors.toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
利用 CompletableFuture 向其提交任务执行是个不错的主意。处理需大量使用异步操作的情况时,这几乎是最有效的策略。
构造同步和异步操作:
public List <String> findPrices(String product) {
List <CompletableFuture <String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor)))
.collect(Collectors.toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
CompletableFuture 执行流程.png
Java8 的 CompletableFuture API 提供了名为 thenCompose 的方法,它就是专门为这一目的而设计的, thenCompose 方法允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。创建两个 CompletableFuture 对象,对第一个 CompletableFuture 对象调用 thenCompose ,并向其传递一个函数。当第一个CompletableFuture 执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个 CompletableFuture 的返回做输入计算出的第二个 CompletableFuture 对象。thenCompose 方法像 CompletableFuture 类中的其他方法一样,也提供了一个以 Async 后缀结尾的版本 thenComposeAsync 。通常而言,名称中不带 Async 的方法和它的前一个任务一样,在同一个线程中运行;而名称以 Async 结尾的方法会将后续的任务提交到一个线程池,所以每个任务是由不同的线程处理的。
方法名 | 描述 |
---|---|
allOf(CompletableFuture<?>... cfs) | 等待所有任务完成,构造后CompletableFuture完成 |
anyOf(CompletableFuture<?>... cfs) | 只要有一个任务完成,构造后CompletableFuture就完成 |
runAsync(Runnable runnable) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码 |
runAsync(Runnable runnable, Executor executor) | 使用指定的thread pool执行异步代码 |
supplyAsync(Supplier<U> supplier) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作有返回值 |
supplyAsync(Supplier<U> supplier,Executor executor) | 使用指定的thread pool执行异步代码,异步操作有返回值 |
complete(T t) | 完成异步执行,并返回future的结果 |
completeExceptionlly(Throwable ex) | 异步执行不正常的结束 |
cancel(boolean mayInterruptIfRunning) | 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束 |
isCancelled() | 任务是否已经取消,任务正常完成前将其取消,则返回 true |
isDone() | 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true |
get() | throws InterruptedException, ExecutionException 等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException |
get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException | 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计 算超时,将抛出TimeoutException |
thenApply(Function<? super T,? extends U> fn) | 转换一个新的CompletableFuture对象 |
thenApplyAsync(Function<? super T,? extends U> fn) | 异步转换一个新的CompletableFuture对象 |
thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) | 使用指定的thread pool执行异步代码,异步转换一个新的CompletableFuture对象 |
thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) | 在异步操作完成的时候对异步操作的结果进行一些操作,并且仍然返回CompletableFuture类型 |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) | 在异步操作完成的时候对异步操作的结果进行一些操作,并且仍然返回CompletableFuture类型。使用ForkJoinPool |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) | 在异步操作完成的时候对异步操作的结果进行一些操作,并且仍然返回CompletableFuture类型。使用指定的线程池 |
thenAccept(Consumer<? super T> action) | 当CompletableFuture完成计算结果,只对结果执行Action,而不返回新的计算值 |
thenAcceptAsync(Consumer<? super T> action) | 当CompletableFuture完成计算结果,只对结果执行Action,而不返回新的计算值,使用ForkJoinPool |
thenAcceptAsync(Consumer<? super T> action,Executor executor) | 当CompletableFuture完成计算结果,只对结果执行Action,而不返回新的计算值 |
thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 当两个CompletableFuture都正常完成后,执行提供的fn,用它来组合另外一个CompletableFuture的结果 |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 当两个CompletableFuture都正常完成后,执行提供的fn,用它来组合另外一个CompletableFuture的结果,使用ForkJoinPool |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor) | 当两个CompletableFuture都正常完成后,执行提供的fn,用它来组合另外一个CompletableFuture的结果,使用指定的线程池 |
thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 当两个CompletableFuture都正常完成后,执行提供的action,用它来组合另外一个CompletableFuture的结果 |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 当两个CompletableFuture都正常完成后,执行提供的action,用它来组合另外一个CompletableFuture的结果,使用ForkJoinPool |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action, Executor executor) | 当两个CompletableFuture都正常完成后,执行提供的action,用它来组合另外一个CompletableFuture的结果,使用指定的线程池 |
whenComplete(BiConsumer<? super T,? super Throwable> action) | 当CompletableFuture完成计算结果时对结果进行处理,或者当CompletableFuture产生异常的时候对异常进行处理 |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) | 当CompletableFuture完成计算结果时对结果进行处理,或者当CompletableFuture产生异常的时候对异常进行处理,使用ForkJoinPool |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor) | 当CompletableFuture完成计算结果时对结果进行处理,或者当CompletableFuture产生异常的时候对异常进行处理,使用指定的线程池。 |
handle(BiFunction<? super T, Throwable, ? extends U> fn) | 当CompletableFuture完成计算结果或者抛出异常的时候,执行提供的fn |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) | 当CompletableFuture完成计算结果或者抛出异常的时候,执行提供的fn,使用ForkJoinPool |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) | 当CompletableFuture完成计算结果或者抛出异常的时候,执行提供的fn,使用指定的线程池 |
- thenApply 的功能相当于将 CompletableFuture<T> 转换成 CompletableFuture<U>
- thenCompose 可以用于组合多个 CompletableFuture,将前一个结果作为下一个计算的参数,它们之间存在着先后顺序
- 现在有 CompletableFuture<T>、CompletableFuture<U> 和一个函数(T,U)->V,thenCompose 就是将 CompletableFuture<T> 和 CompletableFuture<U> 变为 CompletableFuture<V>
- 使用 thenCombine() 之后 future1、future2 之间是并行执行的,最后再将结果汇总。这一点跟 thenCompose() 不同
- thenAcceptBoth 跟 thenCombine 类似,但是返回 CompletableFuture<Void> 类型
- handle() 的参数是 BiFunction,apply() 方法返回 R,相当于转换的操作
- whenComplete() 的参数是 BiConsumer,accept() 方法返回 void
- thenAccept() 是只会对计算结果进行消费而不会返回任何结果的方法
时间API
Clock
Clock 类提供了访问当前日期和时间的方法,Clock 是时区敏感的,可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用Instant 类来表示,Instant 类也可以用来创建老的 java.util.Date 对象。
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);
LocalDate
该类的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。通过静态工厂方法 of 创建一个 LocalDate 实例。 LocalDate 实例提供了多种方法来读取常用的值,比如年份、月份、星期几等。
LocalDate date = LocalDate.of(2018, 10, 1);
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek();
int len = date.lengthOfMonth();
boolean leap = date.isLeapYear();
等同于
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
获取当前时间:
LocalDate today = LocalDate.now();
LocalTime
一天中的时间,比如13:45:20,可以使用 LocalTime 类表示。你可以使用 of 重载的两个工厂方法创建 LocalTime 的实例。 第一个重载函数接收小时和分钟, 第二个重载函数同时还接收秒。同 LocalDate 一样, LocalTime 类也提供了一些 getter 方法访问这些变量的值。
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();
LocalDate 和 LocalTime 都可以通过解析代表它们的字符串创建。使用静态方法 parse:
LocalDate date = LocalDate.parse("2018-03-18");
LocalTime time = LocalTime.parse("13:45:20");
可以向 parse 方法传递一个 DateTimeFormatter 。该类的实例定义了如何格式化一个日
期或者时间对象。它是替换老版 java.util.DateFormat 的推荐替代品。一旦传递的字符串参数无法被解析为合法的 LocalDate 或 LocalTime 对象, 这两个 parse 方法都会抛出一个继承自 RuntimeException 的 DateTimeParseException 异常。
LocalDateTime
这个复合类名叫 LocalDateTime ,是 LocalDate 和 LocalTime 的合体。它同时表示了日期和时间, 但不带有时区信息, 你可以直接创建, 也可以通过合并日期和时间对象构造。
LocalDateTime dt1 = LocalDateTime.of(2018, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
通过它们各自的 atTime 或者 atDate 方法,向 LocalDate 传递一个时间对象,或者向 LocalTime 传递一个日期对象的方式,你可以创建一个 LocalDateTime 对象。你也可以使用toLocalDate 或者 toLocalTime 方法,从 LocalDateTime 中提取 LocalDate 或者 LocalTime组件:
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
Instant
可以通过向静态工厂方法 ofEpochSecond 传递一个代表秒数的值创建一个该类的实例。 静态工厂方法 ofEpochSecond 还有一个增强的重载版本,它接收第二个以纳秒为单位的参数值,对传入作为秒数的参数进行调整。重载的版本会调整纳秒参数,确保保存的纳秒分片在0到999 999999之间。
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); // 2秒 之 后 再 加上100万纳秒(1秒)
Instant.ofEpochSecond(4, -1_000_000_000); // 4秒之前的100万纳秒(1秒)
修改操作:
如果你已经有一个 LocalDate 对象, 想要创建它的一个修改版, 最直接也最简单的方法是使用 withAttribute 方法。 withAttribute 方法会创建对象的一个副本,并按照需要修改它的属性。
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.withYear(2011); // 2011-03-18
LocalDate date3 = date2.withDayOfMonth(25); // 2011-03-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); // 2011-09-25
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.plusWeeks(1); // 2014-03-25
LocalDate date3 = date2.minusYears(3); // 2011-03-25
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2011-09-25
LocalDate 、 LocalTime 、 LocalDateTime 以及 Instant 通用方法
方法名 | 是否是静态方法 | 描述 |
---|---|---|
from | 是 | 依据传入的 Temporal 对象创建对象实例 |
now | 是 | 依据系统时钟创建 Temporal 对象 |
of | 是 | 由 Temporal 对象的某个部分创建该对象的实例 |
parse | 是 | 由字符串创建 Temporal 对象的实例 |
atOffset | 否 | 将 Temporal 对象和某个时区偏移相结合 |
atZone | 否 | 将 Temporal 对象和某个时区相结合 |
format | 否 | 使用某个指定的格式器将 Temporal 对象转换为字符串 ( Instant 类不提供该方法) |
get | 否 | 读取 Temporal 对象的某一部分的值 |
minus | 否 | 创建 Temporal 对象的一个副本, 通过将当前 Temporal 对象的值减去一定的时长创建该副本 |
plus | 否 | 创建 Temporal 对象的一个副本, 通过将当前 Temporal 对象的值加上一定的时长创建该副本 |
with | 否 | 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本 |
LocalDate date = LocalDate.of(2014, 3, 18);
date = date.with(ChronoField.MONTH_OF_YEAR, 9);
date = date.plusYears(2).minusDays(10);
date.withYear(2011);
答案: 2016-09-08 。
每个动作都会创建一个新的 LocalDate 对象,后续的方法调用可以操纵前一方法创建的对象。这段代码的最后一句不会产生任何我们能看到的效果,因为它像前面的那些操作一样,会创建一个新的 LocalDate 实例,不过我们并没有将这个新创建的值赋给任何的变量。
Duration
用于比较 LocalTime 之间的时间差, Duration 类主要用于以秒和纳秒衡量时间的长短。
LocalTime time1 = LocalTime.now();
LocalTime time2 = LocalTime.of(11, 0, 0);
Duration d1 = Duration.between(time1, time2);
Period
用于比较 LocalDate 之间的时间差, Period 类主要用于以年月日衡量时间的长短。
Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18));
Duration 和 Period 通用方法
方法名 | 是否是静态方法 | 方法描述 |
---|---|---|
between | 是 | 创建两个时间点之间的 interval |
from | 是 | 由一个临时时间点创建 interval |
of | 是 | 由它的组成部分创建 interval的实例 |
parse | 是 | 由字符串创建 interval 的实例 |
addTo | 否 | 创建该 interval 的副本,并将其叠加到某个指定的 temporal 对象 |
get | 否 | 读取该 interval 的状态 |
isNegative | 否 | 检查该 interval 是否为负值,不包含零 |
isZero | 否 | 检查该 interval 的时长是否为零 |
minus | 否 | 通过减去一定的时间创建该 interval 的副本 |
multipliedBy | 否 | 将 interval 的值乘以某个标量创建该 interval 的副本 |
negated | 否 | 以忽略某个时长的方式创建该 interval 的副本 |
plus | 否 | 以增加某个指定的时长的方式创建该 interval 的副本 |
subtractFrom | 否 | 从指定的 temporal 对象中减去该 interval |
TemporalAdjuster
将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可以使用重载版本的 with 方法, 向其传递一个提供了更多定制化选择的 TemporalAdjuster 对象,更加灵活地处理日期。
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());
方法名 | 描述 |
---|---|
dayOfWeekInMonth | 创建一个新的日期,它的值为同一个月中每一周的第几天 |
firstDayOfMonth | 创建一个新的日期,它的值为当月的第一天 |
firstDayOfNextMonth | 创建一个新的日期,它的值为下月的第一天 |
firstDayOfNextYear | 创建一个新的日期,它的值为明年的第一天 |
firstDayOfYear | 创建一个新的日期,它的值为当年的第一天 |
firstInMonth | 创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值 |
lastDayOfMonth | 创建一个新的日期,它的值为当月的最后一天 |
lastDayOfNextMonth | 创建一个新的日期,它的值为下月的最后一天 |
lastDayOfNextYear | 创建一个新的日期,它的值为明年的最后一天 |
lastDayOfYear | 创建一个新的日期,它的值为今年的最后一天 |
lastInMonth | 创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值 |
next/previous | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期 |
nextOrSame/previousOrSame | 创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象 |
DateTimeFormatter
处理日期和时间对象时,格式化以及解析日期时间对象是另一个非常重要的功能。新的 java.time.format 包就是特别为这个目的而设计的。这个包中,最重要的类是 DateTime-Formatter。 创建格式器最简单的方法是通过它的静态工厂方法以及常量。 像 BASIC_ISO_DATE和 ISO_LOCAL_DATE 这 样 的 常 量 是 DateTimeFormatter 类 的 预 定 义 实 例 。 所 有 的 DateTimeFormatter 实例都能用于以一定的格式创建代表特定日期或时间的字符串。
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18
等同于
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
和老的 java.util.DateFormat 相比较,所有的 DateTimeFormatter 实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像 DateTimeFormatter 所定义的那些常量,并能在多个线程间共享这些实例。 DateTimeFormatter 类还支持一个静态工厂方法,它可以按照某个特定的模式创建格式器。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);
如果还需要更加细粒度的控制,DateTimeFormatterBuilder 类还提供了更复杂的格式器,你可以选择恰当的方法,一步一步地构造自己的格式器。另外,它还提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确地匹配指定的模式) 、填充,以及在格式器中指定可选节。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
ZoneId
之前看到的日期和时间的种类都不包含时区信息。时区的处理是新版日期和时间 API 新增加的重要功能,使用新版日期和时间 API 时区的处理被极大地简化了。新的 java.time.ZoneId 类是老版 java.util.TimeZone 的替代品。它的设计目标就是要让你无需为时区处理的复杂和繁琐而操心,比如处理日光时(Daylight Saving Time,DST)这种问题。跟其他日期和时间类一样, ZoneId 类也是无法修改的。
ZoneId romeZone = ZoneId.of("Europe/Rome");
地区ID都为 “{区域}/{城市}” 的格式, 这些地区集合的设定都由英特网编号分配机构 (IANA)的时区数据库提供。你可以通过 Java8 的新方法 toZoneId 将一个老的时区对象转换为 ZoneId :
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一个 ZoneId 对象,你就可以将它与 LocalDate 、 LocalDateTime 或者是 Instant 对象整合起来,构造为一个 ZonedDateTime 实例,它代表了相对于指定时区的时间点。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
ZonedDateTime
将 LocalDateTime 转换为 Instant :
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
将 Instant 转换为 LocalDateTime :
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
计算时区
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
日历系统
Java8 中另外还提供了4种其他的日历系统。这些日历系统中的每一个都有一个对应的日志类,分别是 ThaiBuddhistDate 、MinguoDate 、 JapaneseDate 以及 HijrahDate 。所有这些类以及 LocalDate 都实现了 ChronoLocalDate 接口,能够对公历的日期进行建模。利用 LocalDate 对象,你可以创建这些类的实例。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(date);
等同于
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
java8类库
@Repeatable
如果一个注解在设计之初就是可重复的,你可以直接使用它。但是,如果你提供的注解是为用户提供的,那么就需要做一些工作,说明该注解可以重复。
新增方法
类/接口 | 新方法 |
---|---|
Map | getOrDefault , forEach , compute , computeIfAbsent , computeIfPresent , merge ,putIfAbsent , remove(key,value) , replace , replaceAll |
Iterable | forEach , spliterator |
Iterator | forEachRemaining |
Collection | removeIf , stream , parallelStream |
List | replaceAll , sort |
BitSet | stream |
Map
forEach 该方法签名为 void forEach(BiConsumer<? super K,? super V> action),作用是对 Map 中的每个映射执行 action 指定的操作,其中 BiConsumer 是一个函数接口,里面有一个待实现方法 void accept(T t, U u)。
java8之前写法:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
java8:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
getOrDefault 方法就可以替换现在检测 Map 中是否包含给定键映射的惯用方法。如果 Map 中不存在这样的键映射,你可以提供一个默认值,方法会返回该默认值。
java8 之前写法:
Map<String, Integer> carInventory = new HashMap<>();
Integer count = 0;
if (map.containsKey("Aston Martin")) {
count = map.get("Aston Martin");
}
java8:
Integer count = map.getOrDefault("Aston Martin", 0);
putIfAbsent 方法签名为V putIfAbsent(K key, V value),作用是只有在不存在 key 值的映射或映射值为 null 时,才将 value 指定的值放入到 Map 中,否则不对 Map 做更改.该方法将条件判断和赋值合二为一,使用起来更加方便。
remove(Object key, Object value) 方法,只有在当前 Map 中 key 正好映射到 value 时才删除该映射,否则什么也不做。
replace 在 Java7 及以前,要想替换 Map 中的映射关系可通过 put(K key, V value) 方法实现,该方法总是会用新值替换原来的值.为了更精确的控制替换行为,Java8 在 Map 中加入了两个 replace() 方法,分别如下:
- replace(K key, V value),只有在当前 Map 中 key 的映射存在时才用 value 去替换原来的值,否则什么也不做。
- replace(K key, V oldValue, V newValue),只有在当前 Map 中 key 的映射存在且等于 oldValue 时才用 newValue 去替换原来的值,否则什么也不做。
replaceAll 该方法签名为 replaceAll(BiFunction<? super K,? super V,? extends V> function),作用是对 Map 中的每个映射执行 function 指定的操作,并用 function 的执行结果替换原来的 value,其中 BiFunction 是一个函数接口,里面有一个待实现方法 R apply(T t, U u)。
java8 之前写法:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
entry.setValue(entry.getValue().toUpperCase());
}
java8:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());
merge 该方法签名为 merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction),作用是:
如果 Map 中 key 对应的映射不存在或者为 null,则将 value(不能是 null)关联到 key 上;
否则执行 remappingFunction,如果执行结果非 null 则用该结果跟 key 关联,否则在 Map 中删除 key 的映射。
Map<String, String> myMap = new HashMap<>();
myMap.put("A", "str01A");
myMap.merge("A", "merge01", String::concat); // str01A merge01
compute 该方法签名为 compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) ,如果 map 里有这个 key,那么 remappingFunction 输入的 v 就是现在的值,返回的是对应 value,如果没有这个 key,那么输入的 v 是 null。
map.compute(key, (k, v) -> v == null ? newMsg : v.concat(newMsg));
computeIfAbsent 该方法签名为 V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction),作用是:只有在当前 Map 中不存在 key 值的映射或映射值为 null 时,才调用 mappingFunction,并在 mappingFunction 执行结果非 null 时,将结果跟 key 关联。
java8 之前写法:
Map<Integer, Set<String>> map = new HashMap<>();
if (map.containsKey(1)) {
map.get(1).add("one");
} else {
Set<String> valueSet = new HashSet<String>();
valueSet.add("one");
map.put(1, valueSet);
}
java8:
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");
computeIfPresent 该方法签名为 V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),作用跟 computeIfAbsent() 相反,即,只有在当前 Map 中存在 key 值的映射且非 null 时,才调用 remappingFunction,如果 remappingFunction 执行结果为 null,则删除 key 的映射,否则使用该结果替换 key 原来的映射。
Collection
removeIf 该方法签名为 boolean removeIf(Predicate<? super E> filter),作用是删除容器中所有满足 filter 指定条件的元素,其中 Predicate 是一个函数接口,里面只有一个待实现方法 boolean test(T t),同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。
java8之前写法:
// 使用迭代器删除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().length()>3) { // 删除长度大于3的元素
it.remove();
}
}
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
// 删除长度大于3的元素
list.removeIf(str -> str.length() > 3);
replaceAll 该方法签名为 void replaceAll(UnaryOperator<E> operator),作用是对每个元素执行 operator 指定的操作,并用操作结果来替换原来的元素。其中 UnaryOperator 是一个函数接口,里面只有一个待实现函数 T apply(T t)。
java8 之前写法:
// 使用下标实现元素替换
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for (int i=0; i<list.size(); i++) {
String str = list.get(i);
if (str.length()>3) {
list.set(i, str.toUpperCase());
}
}
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
if (str.length() > 3) {
return str.toUpperCase();
}
return str;
});
sort 该方法定义在List接口中,方法签名为 void sort(Comparator<? super E> c),该方法根据c指定的比较规则对容器元素进行排序。Comparator 接口我们并不陌生,其中有一个方法int compare(T o1, T o2) 需要实现,显然该接口是个函数接口。
java8 之前写法:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>() {
@Override public int compare(String str1, String str2) {
return str1.length() - str2.length();
}
});
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length() - str2.length());
spliterator 方法签名为 Spliterator<E> spliterator(),该方法返回容器的可拆分迭代器。从名字来看该方法跟 iterator() 方法有点像,我们知道Iterator是用来迭代容器的, Spliterator 也有类似作用,但二者有如下不同:
- Spliterator 既可以像 Iterator 那样逐个迭代,也可以批量迭代。批量迭代可以降低迭代的开销。
- Spliterator 是可拆分的,一个 Spliterator 可以通过调用 Spliterator<T> trySplit() 方法来尝试分成两个。一个是 this,另一个是新返回的那个,这两个迭代器代表的元素没有重叠。
可通过(多次)调用 Spliterator.trySplit() 方法来分解负载,以便多线程处理。
stream 和 parallelStream 分别返回该容器的 Stream 视图表示,不同之处在于parallelStream() 返回并行的 Stream。Stream 是 Java 函数式编程的核心类。
并发包
原子操作
java.util.concurrent.atomic 包提供了多个对数字类型进行操作的类,比如 AtomicInteger 和 AtomicLong ,它们支持对单一变量的原子操作。这些类在 Java8 中新增了更多的方法支持。
- getAndUpdate —— 以原子方式用给定的方法更新当前值,并返回变更之前的值。
- updateAndGet —— 以原子方式用给定的方法更新当前值,并返回变更之后的值。
- getAndAccumulate —— 以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之前的值。
- accumulateAndGet —— 以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之后的值。
Adder 和 Accumulator:
多线程的环境中,如果多个线程需要频繁地进行更新操作,且很少有读取的动作(比如,在统计计算的上下文中) ,Java API 文档中推荐大家使用新的类 LongAdder 、 LongAccumulator 、DoubleAdder 以及 DoubleAccumulator ,尽量避免使用它们对应的原子类型。这些新的类在设计之初就考虑了动态增长的需求,可以有效地减少线程间的竞争。
LongAddr 和 DoubleAdder 类都支持加法操作,而 LongAccumulator 和 DoubleAccumulator 可以使用给定的方法整合多个值。
LongAdder adder = new LongAdder();
adder.add(10);
long sum = adder.sum();
等同于
LongAccumulator acc = new LongAccumulator(Long::sum, 0);
acc.accumulate(10);
long result = acc.get();
ConcurrentHashMap
ConcurrentHashMap 类的引入极大地提升了 HashMap 现代化的程度,新引入的ConcurrentHashMap 对并发的支持非常友好。 ConcurrentHashMap 允许并发地进行新增和更新操作,因为它仅对内部数据结构的某些部分上锁。因此,和另一种选择,即同步式的 Hashtable 比较起来,它具有更高的读写性能。
- 性能
为了改善性能,要对 ConcurrentHashMap 的内部数据结构进行调整。典型情况下, map 的条目会被存储在桶中,依据键生成哈希值进行访问。但是,如果大量键返回相同的哈希值,由于桶是由 List 实现的,它的查询复杂度为O(n),这种情况下性能会恶化。在 Java8 中,当桶过于臃肿时,它们会被动态地替换为排序树(sorted tree) ,新的数据结构具有更好的查询性能(排序树的查询复杂度为O(log(n))) 。注意,这种优化只有当键是可以比较的(比如 String 或者 Number类)时才可能发生。 - 类流操作
ConcurrentHashMap 支持三种新的操作,这些操作和你之前在流中所见的很像:
- forEach ——对每个键值对进行特定的操作
- reduce ——使用给定的精简函数(reduction function) ,将所有的键值对整合出一个结果
- search ——对每一个键值对执行一个函数,直到函数的返回值为一个非空值
以上每一种操作都支持四种形式,接受使用键、值、 Map.Entry 以及键值对的函数: - 使用键和值的操作( forEach 、 reduce 、 search )
- 使用键的操作( forEachKey 、 reduceKeys 、 searchKeys )
- 使用值的操作 ( forEachValue 、 reduceValues 、 searchValues )
- 使用 Map.Entry 对象的操作( forEachEntry 、 reduceEntries 、 searchEntries )
注意,这些操作不会对 ConcurrentHashMap 的状态上锁。它们只会在运行过程中对元素进行操作。应用到这些操作上的函数不应该对任何的顺序,或者其他对象,抑或在计算过程发生变化的值,有依赖。
除此之外,你需要为这些操作指定一个并发阈值。如果经过预估当前 map 的大小小于设定的阈值,操作会顺序执行。使用值 1 开启基于通用线程池的最大并行。使用值 Long.MAX_VALUE 设定程序以单线程执行操作。
下面这个例子中,我们使用 reduceValues 试图找出 map 中的最大值:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Optional<Integer> maxValue = Optional.of(map.reduceValues(1, Integer::max));
注意,对 int 、 long 和 double ,它们的 reduce 操作各有不同(比如 reduceValuesToInt 、reduceKeysToLong 等) 。
- 计数
ConcurrentHashMap 类提供了一个新的方法,名叫 mappingCount ,它以长整型 long 返回 map 中映射的数目。我们应该尽量使用这个新方法,而不是老的 size 方法, size 方法返回的类型为 int 。这是因为映射的数量可能是 int 无法表示的。 - 集合视图
ConcurrentHashMap 类还提供了一个名为 KeySet 的新方法,该方法以 Set 的形式返回ConcurrentHashMap 的一个视图(对 map 的修改会反映在该 Set 中,反之亦然) 。你也可以使用新的静态方法 newKeySet ,由 ConcurrentHashMap 创建一个 Set 。
Arrays
使用 parallelSort
parallelSort 方法会以并发的方式对指定的数组进行排序,你可以使用自然顺序,也可以为数组对象定义特别的 Comparator 。
使用 setAll 和 parallelSetAll
setAll 和 parallelSetAll 方法可以以顺序的方式也可以用并发的方式,使用提供的函数计算每一个元素的值,对指定数组中的所有元素进行设置。该函数接受元素的索引,返回该索引元素对应的值。由于 parallelSetAll 需要并发执行,所以提供的函数必须没有任何副作用。
int[] evenNumbers = new int[10];
Arrays.setAll(evenNumbers, i -> i * 2); // 0, 2, 4, 6...
使用 parallelPrefix
parallelPrefix 方法以并发的方式, 用用户提供的二进制操作符对给定数组中的每个元素进行累积计算。
int[] ones = new int[10];
Arrays.fill(ones, 1);
Arrays.parallelPrefix(ones, (a, b) -> a + b);
Number
Number 类中新增的方法如下。
- Short 、 Integer 、 Long 、 Float 和 Double 类提供了静态方法 sum 、 min 和 max 。
- Integer 和 Long 类提供了 compareUnsigned 、 divideUnsigned 、 remainderUnsigned 和 toUnsignedLong 方法来处理无符号数。
- Integer 和 Long 类也分别提供了静态方法 parseUnsignedInt 和 parseUnsignedLong将字符解析为无符号 int 或者 long 类型。
- Byte 和 Short 类提供了 toUnsignedInt 和 toUnsignedLong 方法通过无符号转换将参数转化为 int 或 者 long 类型 。 类似地 , Integer 类现在也提供了静态方法toUnsignedLong 。
- Double 和 Float 类提供了静态方法 isFinite ,可以检查参数是否为有限浮点数。
- Boolean 类现在提供了静态方法 logicalAnd 、 logicalOr 和 logicalXor ,可以在两个boolean 之间执行 and 、 or 和 xor 操作。
- BigInteger 类提供了 byteValueExact 、 shortValueExact 、 intValueExact 和longValueExact 可以将 BigInteger 类型的值转换为对应的基础类型。不过,如果在转换过程中有信息的丢失,方法会抛出算术异常。
Math
如果 Math 中的方法在操作中出现溢出, Math 类提供了新的方法可以抛出算术异常。支持这一异常的方法包括使用 int 和 long 参数的 addExact 、 subtractExact 、 multipleExact 、incrementExact 、 decrementExact 和 negateExact 。此外, Math 类还新增了一个静态方法toIntExact , 可以将 long 值转换为 int 值。 其他的新增内容包括静态方法 floorMod 、 floorDiv和 nextDown 。
Files
Files 类最引人注目的改变是,你现在可以用文件直接产生流。通过 Files.lines 方法你可以以延迟方式读取文件的内容,并将其作为一个流。此外,还有一些非常有用的静态方法可以返回流。
- Files.list —— 生成由指定目录中所有条目构成的 Stream<Path> 。这个列表不是递归包含的。由于流是延迟消费的,处理包含内容非常庞大的目录时,这个方法非常有用。
- Files.walk —— 和 Files.list 有些类似,它也生成包含给定目录中所有条目的Stream<Path> 。不过这个列表是递归的,你可以设定递归的深度。注意,该遍历是依照深度优先进行的。
- Files.find —— 通过递归地遍历一个目录找到符合条件的条目,并生成一个Stream<Path> 对象。
Reflection
Relection 接口的另一个变化是新增了可以查询方法参数信息的API,比如,你现在可以使用新增的 java.lang.reflect.Parameter 类查询方法参数的名称和修饰符,这个类被新的java.lang.reflect.Executable 类所引用, 而 java.lang.reflect.Executable 通用函数和构造函数共享的父类。
String
String 类也新增了一个静态方法,名叫 join 。你大概已经猜出它的功能了,它可以用一个分隔符将多个字符串连接起来。
String authors = String.join(", ", "Raoul", "Mario", "Alan");
PS
泛型
Java类型要么是引用类型(比如 Byte 、 Integer 、 Object 、 List ) ,要么是原始类型(比如 int 、 double 、 byte 、 char ) 。但是泛型(比如 Consumer<T> 中的 T )只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应accept 方法的实现Lambda是 Function接口的 apply 方法的实现的原始类型,叫作拆箱(unboxing) 。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。
工具类库
Guava、Apache和lambdaj
广义归约( Collectors.reducing)
它需要三个参数。
第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言 0 是一个合适的值。
第二个参数就是转换成一个表示其所含热量的 int 。
第三个参数是一个 BinaryOperator ,将两个项目累积成一个同类型的值。
求和:
int totalCalories = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));
找出集合中最大值:
Optional <Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
Collectors 类的静态工厂方法
工厂方法 | 返回类型 | 描 述 | 使用示例 |
---|---|---|---|
toList | List<T> | 把流中所有项目收集到一个 List | List<Dish> dishes = menuStream.collect(Collectors.toList()); |
toSet | Set<T> | 把流中所有项目收集到一个 Set ,删除重复项 | Set<Dish> dishes = menuStream.collect(Collectors.toSet()); |
toMap | Map<T, K> | 把流中所有项目收集到一个 Map ,删除重复项,默认情况下,出现重复数据会报错 | Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories));如有重复数据,可以设置使用哪一个数据 Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories, d -> d, (d1, d2) -> d1, LinkedHashMap::new)); |
toCollection | Collection<T> | 把流中所有项目收集到给定的供应源创建的集合 | Collection<Dish> dishes = menuStream.collect(Collectors.toCollection(), ArrayList::new); |
counting | Long | 计算流中元素的个数 | long howManyDishes = menuStream.collect(Collectors.counting()); |
summingInt | Integer | 对流中项目的一个整数属性求和 | int totalCalories = menuStream.collect(Collectors.summingInt(Dish::getCalories)); |
averagingInt | Integer | 计算流中项目 Integer 属性的平均值 | int avgCalories = menuStream.collect(Collectors.averagingInt(Dish::getCalories)); |
summarizingInt | IntSummaryStatistics | 收集关于流中项目 Integer 属性的统计值,例如最大、最小、总和与平均值 | IntSummaryStatistics menuStatistics = menuStream.collect(Collectors.summarizingInt(Dish::getCalories)); |
joining | String | 连接对流中每个项目调用 toString 方法所生成的字符串 | String shortMenu = menuStream.map(Dish::getName).collect(Collectors.joining(", ")); |
maxBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最大元素的 Optional ,或如果流为空则为 Optional.empty() | Optional<Dish> fattest = menuStream.collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))); |
minBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最小元素的 Optional ,或如果流为空则为 Optional.empty() | Optional<Dish> lightest = menuStream.collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories))); |
reducing | 归约操作产生的类型 | 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值 | int totalCalories = menuStream.collect(Collectors.reducing(0, Dish::getCalories, Integer::sum)); |
collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 | int howManyDishes = menuStream.collect(Collectors.collectingAndThen(toList(), List::size)); |
groupingBy | Map<K, List<T>> | 根据项目的一个属性的值对流中的项目分组组,并将属性值分组结果 Map 的键 | Map<Dish.Type,List<Dish>> dishesByType = menuStream.collect(Collectors.groupingBy(Dish::getType)); |
partitioningBy | Map<Boolean,List<T>> | 根据对流中每个项目应用谓词的结果来对项目进行分区 | Map<Boolean,List<Dish>> vegetarianDishes = menuStream.collect(Collectors.partitioningBy(Dish::isVegetarian)); |
Optional介绍
Optional<T> 类( java.util.Optional )是一个容器类,代表一个值存在或不存在。
- isPresent() 将在 Optional 包含值的时候返回 true , 否则返回 false 。
- ifPresent(Consumer<T> block) 会在值存在的时候执行给定的代码块。
- T get() 会在值存在时返回值,否则抛出一个 NoSuchElement 异常。
- T orElse(T other) 会在值存在时返回值,否则返回一个默认值。
线程个数计算方式
如果线程池中线程的数量过多,最终它们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,正如你的应用所面临的情况,处理器的一些核可能就无法充分利用。Brian Goetz建议,线程池大小与处理器的利用率之比可以使用下面的公式进行估算:
N threads = N CPU * U CPU * (1 + W/C)
其中:
- N CPU 是处理器的核的数目,可以通过 Runtime.getRuntime().availableProcessors() 得到
- U CPU 是期望的CPU利用率(该值应该介于0和1之间)
- W/C 是等待时间与计算时间的比率
并行——使用 parallelStream 还是 CompletableFutures ?
目前为止, 你已经知道对集合进行并行计算有两种方式: 要么将其转化为parallelStream, 利用 map 这样的操作开展工作,要么枚举出集合中的每一个元素,创建新的线程,在 CompletableFuture 内对其进行操作。后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助你确保整体的计算不会因为线程都在等待 I/O 而发生阻塞。
我们对使用这些 API 的建议如下。
如果你进行的是计算密集型的操作,并且没有 I/O,那么推荐使用 Stream 接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程) 。
反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待) ,那么使用 CompletableFuture 灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者 W/C 的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生 I/O 等待, 流的延迟特性会让我们很难判断到底什么时候触发了等待。
配置并行流使用的线程池
并行流内部使用了默认的 ForkJoinPool,它默认的线程数量就是你的处理器数量 , 这个值是由 Runtime.getRuntime().availableProcessors() 得到的。
但是可以通 过系统属性 java.util.concurrent.ForkJoinPool.common.parallelism 来改变线程池大小,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个
并行流指定这个值。一般而言,让 ForkJoinPool 的大小等于处理器数量是个不错的默认值,
除非你有很好的理由,否则我们强烈建议你不要修改它。
测量流性能
我们声称并行求和方法应该比顺序和迭代方法性能好。然而在软件工程上,靠猜绝对不是什么好办法!特别是在优化性能时,你应该始终遵循三个黄金规则:测量,测量,再测量。
- 并行流并不总是比顺序流快。
有些操作本身在并行流上的性能就比顺序流差。特别是 limit 和 findFirst 等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。例如, findAny 会比 findFirst 性能好,因为它不一定要按顺序来执行。你总是可以调用 unordered 方法来把有序流变成无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用 limit 可能会比单个有序流(比如数据源是一个 List)更高效。 - 留意装箱。自动装箱和拆箱操作会大大降低性能。Java8 中有原始类型流(IntStream 、LongStream 、 DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
- 考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味着使用并行流时性能好的可能性比较大。
- 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素的好处还抵不上并行化造成的额外开销。
- 考虑流背后的数据结构是否易于分解。例如, ArrayList 的拆分效率比 LinkedList 高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用 range 工厂方法创建的原始类型流也可以快速分解。
可分解性总结了一些流数据源适不适于并行:
源 | 可分解性 |
---|---|
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |