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

ZonedDateTime.png

将 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 比较起来,它具有更高的读写性能。

  1. 性能
    为了改善性能,要对 ConcurrentHashMap 的内部数据结构进行调整。典型情况下, map 的条目会被存储在桶中,依据键生成哈希值进行访问。但是,如果大量键返回相同的哈希值,由于桶是由 List 实现的,它的查询复杂度为O(n),这种情况下性能会恶化。在 Java8 中,当桶过于臃肿时,它们会被动态地替换为排序树(sorted tree) ,新的数据结构具有更好的查询性能(排序树的查询复杂度为O(log(n))) 。注意,这种优化只有当键是可以比较的(比如 String 或者 Number类)时才可能发生。
  2. 类流操作
    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 等) 。

  1. 计数
    ConcurrentHashMap 类提供了一个新的方法,名叫 mappingCount ,它以长整型 long 返回 map 中映射的数目。我们应该尽量使用这个新方法,而不是老的 size 方法, size 方法返回的类型为 int 。这是因为映射的数量可能是 int 无法表示的。
  2. 集合视图
    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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容