Skip to content

Java 8 实战

为什么使用 Java 8

并行处理中 Stream API(Stream.parallelStream()方法) 相对于 java 8 之前的 Thread API

  • Java8 之前: 对集合的操作为了提高多核 CPU 的性能,只能使用 Thread API 进行并行处理, 但使用 Thread API 来并行处理并非易事。并且为了处理并行处理的线程安全问题,使用 synchronized 关键字,要是用错了地方,就可能出现很多难以察觉的错误。
  • Java8 之后:使用 Stream API, 程序员不用再关心并行处理以及多核 CPU 并行处理时的线程安全问题了,并且代码写起来更加简洁。
  • Java8 用 Stream API 解决了两个问题:集合处理时复杂的套路和晦涩,以及难以利用的多核。

default 关键词 —— 在接口中定义方法的默认实现

这样子类可以不需要显示的实现 sort 方法。

如果子类实现了默认的 sort 方法,则方法被重写。否则当应用运行时,就会使用接口中默认的实现逻辑。

java
public interface Collection {

    default void sort(Comparator<? super E> c) {
        Collections.sort(this, c);
    }

}

Lambda 表达式

Lambda 示例

使用案例Lambda 示例
布尔表达式(List<String> list) -> list.isEmpty()
创建对象() -> new Apple(10)
消费一个对象(Apple a) ->
从一个对象中选择/抽取(String s) -> s.length()
组合两个值(int a, int b) -> a * b
比较两个对象(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

Lambda 表达式实例

用 Comparator 来排序

Java 8 之前:

java
List<Apple> inventory = new ArrayList();

inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a2, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

使用 Lambda 表达式:

java
inventory.sort((Apple a2, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

用 Runnable 执行代码块

Java 8 之前:

java
Thread t = new Thread(new Runnable() {
    public voi run() {
        System.out.println("Hello world");
    }
});

使用 Lambda 表达式:

java
Thread t = new Thread(() -> System.out.println("Hello world"));

函数式接口

java
// java.util.function
public interface Predicate<T> {
    boolean test(T t);
}

// java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// java.lang.Runnable
public interface Runnable<T> {
    void run();
}

// java.util.concurrent.Callable
public interface Callable<V> {
    V call();
}

// java.security.PrivilegedAction
public interface PrivilegedAction<V> {
    V run();
}

@FunctionalInterface

函数式接口上带有 @FunctionalInterface 注解,表示该接口会设计成一个函数式接口。如果你用这个注解定义了一个接口, 而它却不是函数式接口的话,编译器将返回一个提示原因的错误。请注意, 这个注解不是必须的,它类似于 @Override 注解也不是必须的,但是具有明确的意义,因而使用它时比较好的做法。

使用函数式接口

Predicate

java
// java.util.function
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList();
    for(T s: list) {
        if(p.test(s)) {
            results.add(s);
        }
    }
    return results;
}

Predicate<String> predicateImpl = (String s) -> !s.isEmpty();
List<String> stringList = Arrays.asList("a", "b", "");
List<String> nonEmpt = filter(stringList, predicateImpl);

Consumer

java
// java.util.function
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

public static <T> void forEach(List<T> list, Consumer<T> c) {
    List<T> results = new ArrayList();
    for(T i: list) {
        c.accept(i);
    }
}

List<String> stringList = Arrays.asList("a", "b", "c");
forEach(stringList, (String s) -> System.out.println(s));

Function

java
// java.util.function
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> results = new ArrayList();
    for(T s: list) {
        results.add(f.apply(s));
    }
    return result;
}

List<String> stringList = Arrays.asList("a", "b", "c");
List<Integer> integerList = map(stringList, (String s) -> s.length());

基本类型避免拆装箱的函数式接口

为什么要避免基本类型的拆装箱?

因为装箱的本质是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

而上面提供的 Predicate<T>、Consumer<T>、Function<T, R> 中的泛型参数只能绑定到引用类型,不能绑定到基本类型,这是由于泛型内部实现方式造成的。

Java 8 对于基本类型的函数式接口带来了专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。

IntPredicate

java
@FunctionalInterface
public interface IntPredicate {
    boolean test(int t);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);  // true(无装箱)

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);  // false(装箱)

Java 8 中的常用函数式接口

函数式接口函数描述符原始类型特化
Predicate<T>T->booleanIntPredicate, LongPredicate, DoublePredicate
Consumer<T>T->voidIntConsumer, LongConsumer, DoubleConsumer
Function<T, R>T->RIntFunction<R>,
IntToDoubleFunction,
IntToLongFunction,
LongFunction<R>,
LongToDoubleFunction,
LongToIntFunction,
DoubleFunction<R>,
ToIntFunction<T>,
ToDoubleFunction<T>,
ToLongFuncion<T>
Supplier<T>()->TBooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator<T>T->TIntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator<T>(T, T)->TIntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L, R>(L, R)->boolean
BiConsumer<T, U>(T, U)->voidObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T, U, R>(T, U)->RToIntBiFunction<T, U>, ToLongBiFunction<T, U>, ToDoubleBiFunction<T, U>

TIP

  1. Function<T, R>: 一般用于将类型 T 的对象转换为类型 R 的对象(比如 Function<Apple, Integer> 用来提取苹果的重量)
  2. IntBinaryOperator:具有唯一一个抽象方法 applyAsInt。
  3. Consumer<T>:具有唯一一个抽象方法 accept。
  4. Supplier<T>:具有唯一一个抽象方法 get。一般用于创建对象。如: Supplier<Product> loanSupplier = Loan::new; Loan loan = loanSupplier.get();
  5. BiFunction<T, U, R>:具有唯一一个抽象方法 apply。

Lambdas 及函数式接口的例子

使用案例Lambda 的例子对应的函数式接口
布尔表达式(List<String> list) -> list.isEmpty()Predicate<List<String>>
创建对象() -> new Apple(10)Supplier<Apple>
消费一个对象(Apple a) -> System.out.println(a.getWeight())Consumer<Apple>
从一个对象中选择/提取(String s) -> s.length()Function<String, Integer> 或 ToIntFunction<String>
合并两个值(int a, int b) -> a * bIntBinaryOperator
比较两个对象(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())Comparator<Apple> 或 BiFunction<Apple, Apple, Integer> 或 ToIntBiFunction<Apple, Apple>

异常,Lambda,函数式接口

请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要 Lambda 表达式来抛出异常,有两种办法:

  1. 定义一个自己的函数式接口,并声明受检异常。
  2. 把 Lambda 包在一个 try/catch 块中。

(1)比如:

java
@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();

(2)但你可能是在使用一个接受函数式接口的 API, 比如 Function<T, R>, 没有办法自己创建一个,这种情况下,可以显式捕获受检异常:

java
Function<BufferedReader, String> f = (BufferedReader b) -> {
    try {
        return b.readLine();
    } catch(IOException e) {
        throw new RuntimeException(e);
    }
}

类型推断

java
// 没有类型推断
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 有类型推断,代码更简洁。
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

TIP

注意:有时候显式的写出类型更易读,有时候去掉它们更易读,程序员必须做出自己的选择。

方法引用

java
// 先前:
inventory.sort(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 使用方法引用后:
inventory.sort(comparing(Apple::getWeight));

Lambda 及其等效方法引用的例子

Lambda等效的方法引用
(Apple a) -> a.getWeight()Apple::getWeight
() -> Thread.currentThread().dumpStack()Thread.currentThread()::dumpStack
(str, i) -> str.substring(i)String::substring
(String s) -> System.out.println(s)System.out::println

如何构建方法引用

  1. 指向静态方法的方法引用。如: Integer::parseInt
  2. 指向任意类型的实例方法的方法引用。如:String 的 length 方法写作:String::length
  3. 指向现有对象的实例方法的方法引用。假设你有一个局部变量 expensiveTransaction 用于存放 Transaction 类型的对象,它支持实例方法 getValue, 那么你就可以写 expensiveTransaction::getValue。等价于 () -> expensiveTransaction.getValue().

举例:

java
List<String> str = Arrays.asList("a", "b", "A");
str.sort(s1, s2) -> s1.compareToIgnoreCase(s2);

改为方法引用:
str.sort(String::compareToIgnoreCase);

构造函数引用

对于无参构造函数,适合 Supplier 函数式接口签名

java
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

改为方法引用:
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();

对于有一个参数的构造函数,适合 Function 函数式接口签名

java
Function<Integer, Apple> c2 = () -> new Apple(weight);
Apple a2 = c2.apply(110);

改为方法引用:
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

对于有两个参数的构造函数,适合 BiFunction 函数式接口签名

java
BiFunction<String, Integer, Apple> c3 = () -> new Apple(color, weight);
Apple a3 = c3.apply("green", 110);

改为方法引用:
BiFunction<Integer, Apple> c3 = Apple::new;
Apple a3 = c3.apply("green", 110);

对于有三个或更多参数的构造函数,适合什么函数式接口签名?

比如构造函数 Color(int, int, int)。

Java 本身并没有提供这样的函数式接口,但你可以自己提供一个。

java
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply (T t, U u, V v);
}

// 使用:
TriFunction(Integer, Integer, Integer, Color) colorFactory = Color::new;

Lambda 和方法引用实战

Comparator 比较器复合

java
// 苹果按照重量排序(默认升序)
inventory.sort(comparing(Apple::getWeight));

// 重量,降序
inventory.sort(comparing(Apple::getWeight).reversed());

// 苹果按照重量降序排序后,如果重量相等再按照国家排序
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

Predicate 谓词复合

java
// 预先定义了红苹果
Predicate<Apple> redApple = forEach((Apple a) -> "red".equals(a.getColor()));

// 取反。不是红苹果
Predicate<Apple> notRedApple = redApple.negate();

// 红苹果,并且重量 > 150
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);

// 红苹果,并且重量 > 150。 或者绿苹果
Predicate<Apple> redAndHeavyAppleOrGreen =
    redApple.and(a -> a.getWeight() > 150)
            .or(a -> "green".equals(a.getColor()));

TIP

注意:and 和 or 方法是按照在表达式链中的位置,从左向右确定优先级的。

因此,a.or(b).and(c) 可以看作 (a || b) && c。

Function 函数复合

java
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);

// 返回 4。先执行 f, 在执行 g。
int result = h.apply(1);

Function<Integer, Integer> h = f.compose(g);
// 返回 3。先执行 g, 在执行 f。
int result = h.apply(1);

TIP

andThen 方法就类似于流水线,你可以拼接多个 andThen 分别来处理流水线上的每道工序。

函数式数据处理

引入流 Stream API

Stream API 的优点

  • 声明性——更简洁,更易读
  • 可复合——更灵活
  • 可并行——性能更好

TIP

流和迭代器类似,只能遍历一次,遍历完之后,我们就说这个流已经被消费掉了。

流的中间操作

操作类型返回类型操作参数函数描述符目的
filter中间Stream<T>Predicate<T>T -> boolean
map中间Stream<R>Function<T, R>T -> R
flatMap中间Stream<R>Function<T, R>T -> Rflatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
limit中间Stream<T>截断流。流支持 limit(n) 方法,返回一个不超过给定长度的流。
skip中间Stream<T>跳过元素。流支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。注意,limit(n) 和 skip(n) 是互补的。
sorted中间Stream<T>Comparator<T>(T, T) -> int
distinct中间Stream<T>

流的终端操作

操作类型目的
forEach终端消费流中的每个元素并对其应用 Lambda。这一操作返回 void
count终端返回流中元素的个数。这一操作返回 long
collect终端把流归约成一个集合,比如 List、Map 甚至是 Integer。

查找和匹配

操作类型目的示例
allMatch终端allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词。boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
anyMatch终端流中是否有一个元素能匹配给定的谓词if(menu.stream().anyMatch(Dish::isVegetarian)){}
noneMatch终端和 allMatch 相对的是 noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
findFirst终端有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List 或 排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为
findAny终端findAny 方法将返回当前流中的任意元素。

归约

操作类型目的示例
reduce终端把流中所有元素反复结合起来,得到一个值。元素求和:int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum = numbers.stream().reduce(0, Integer::sum);
无初始值:Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

元素相乘:int product = numbers.stream().reduce(1, (a, b) -> a * b);

最大值:Optional<Integer> max = numbers.stream().reduce(Integer::max);

最小值:Optional<Integer> min = numbers.stream().reduce(Integer::min);

并行化求和:<Integer> sum = numbers.parallelStream().reduce(Integer::sum);
collect终端收集所有符合条件的结果集。List<String> traderList = traders.stream().distinct().sorted(comparing(Trader::getName)).collect(toList());

String traderStr = traders.stream().distinct().sorted(comparing(Trader::getName)).reduce("", (n1, n2) -> n1 + n2);
注意,此方案效率不高,所有字符串被反复连接,每次迭代的时候都要建立一个新的 String 对象。

更高效的方案:String traderStr = traders.stream().distinct().sorted().collect(joining());
内部会用到 StringBuilder.
min终端求最小值。int sum = numbers.stream().min();
max终端求最大值。int sum = numbers.stream().max();
sum终端求和。int sum = numbers.stream().sum();

原始类型流特化 —— 避免拆装箱成本

操作类型目的示例
mapToInt中间返回 IntStreamint calories = menu.stream().mapToInt(Dish::getCalories).sum();
boxed中间返回一般流对象装箱操作:
Int intStream = menu.stream().MapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
range中间生成数值范围生成 1 ~ 10 之间的数字,不包括 10:
IntStream intStream = IntStream.range(1, 10);
int count = intStream.filter(n -> n % 2 == 0).count();
rangeClosed中间生成数值范围生成 1 ~ 10 之间的数字,包括 10:
IntStream intStream = IntStream.rangeClosed(1, 10);
int count = intStream.filter(n -> n % 2 == 0).count();
mapToObj中间返回对象值流

flatMap 案例

对给定单词列表 ["Hello","World"],你想返回列表["H","e","l","o","W","r","d"]

String[] words = new String[]{"Hello","World"};
List<String[]> a = Arrays.stream(words)
        .map(word -> word.split(""))
        .distinct()
        .collect(toList());
a.forEach(System.out::print);

代码输出为:[Ljava.lang.String;@12edcd21[Ljava.lang.String;@34c45dca (返回一个包含两个 String[]的 list)

这个实现方式是由问题的,传递给 map 方法的 lambda 为每个单词生成了一个 String[]。因此,map 返回的流实际上是 Stream<String[]> 类型的。你真正想要的是用 Stream<String>来表示一个字符串。

正确方式:flatMap(对流扁平化处理)

String[] words = new String[]{"Hello","World"};
List<String> a = Arrays.stream(words)
        .map(word -> word.split(""))
        .flatMap(Arrays::stream)
        .distinct()
        .collect(toList());
a.forEach(System.out::print);

结果输出:HeloWrd

使用 flatMap 方法的效果是,各个数组并不是分别映射一个流,而是映射成流的内容,所有使用 map(Array::stream)时生成的单个流被合并起来,即扁平化为一个流。

一言以蔽之,flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

短路求值

anyMatch、allMatch 和 noneMatch 这三个操作都用到了我们所谓的短路,这就是大家熟悉 的 Java 中&&和||运算符短路在流中的版本。

何时使用 findFirst 和 findAny?

你可能会想,为什么会同时有 findFirst 和 findAny 呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用 findAny,因为它在使用并行流时限制较少。

构建流

构建一个流

java
Stream<String> stream = Stream.of("A", "B", "C");
stream.map(String::toLowerCase).forEach(System.out::println);

构建一个空流

java
Stream<String> stream = Stream.empty();

由数组创建流

java
int[] numbers = {1, 2, 3};
int sum = Arrays.stream(numbers).sum();

由文件生成流

java
//java.nio.file.Files.java 查看文件中有多少各不相同的词:
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                       .distinct()
                       .count();
} catch(IOException e)
}

由函数生成流,创建无限流

java
// Stream.iterate 和 Stream.generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽的计算下去。
// 一般来说,应该使用 limit(n) 来对这种流加以限制
// 1 iterate 迭代:打印 前 10 个正偶数流
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
// 打印斐波那契数列:
Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t{1}}).limit(10).forEach(t -> Systemout.println(t[0] + ", " + t[1]));

// 2 generate:generate 不是依次对每个新生成的值应用函数的。
Stream.generate(Math::random).limit(5).forEach(System.out::println);

用流收集数据

分组 groupingBy

java
Map<Currency, List<Transaction>> transactionByCurrencies = transactions.stream().collection(groupingBy(Transaction::getCurrency));

收集 toList

java
List<Transaction> transactions =   transactionStream.collect(Collectors.toList());

计数 count

java
long howManyDishes = menu.stream().collect(Collectors.counting());
//这还可以写得更为直接:
long howManyDishes = menu.stream().count();

查找流中的最大值(Collectors.maxBy)和最小值(Collectors.minBy)

java
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
Optional<Dish> lowCalorieDish = menu.stream().collect(minBy(dishCaloriesComparator));

汇总(求和) Collectors.summingInt

java
// Collectors.summingLong和Collectors.summingDouble方法的作用完全一样
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

平均值 Collectors.averagingInt

java
// 连同对应的averagingLong和 averagingDouble可以计算数值的平均数:
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));

通过一次 summarizing 操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值

java
// 同样,相应的summarizingLong和summarizingDouble工厂方法
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
// 获取:
IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}

连接字符串 joining

java
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));

广义的归约汇总

java
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));

int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, Integer::sum));

多级分组

java
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().
    collect(groupingBy(Dish::getType,
        groupingBy(dish -> {
            if(dish.getCalories() <= 400) return CaloricLevel.DIET;
            else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
            else return CaloricLevel.FAT;
        })
    ));

按子组收集数据

java
// 传递给第一个groupingBy的第二个收集器可以是任何类型
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));

Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));

把收集器的结果转换为另一种类型 Collectors.collectingAndThen

java
// 查找每个子组中热量最高的Dish
Map<Dish.Type, Dish> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType,  collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));

与 groupingBy 联合使用的其他收集器的例子 summingInt 和 mapping

java
// 汇总 summingInt
Map<Dish.Type, Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));

// 映射
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =  menu.stream()
    .collect(groupingBy(Dish::getType, mapping(dish -> {
        if (dish.getCalories() <= 400) return CaloricLevel.DIET;
        else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
        else return CaloricLevel.FAT;
    }, toSet())));

分区 partitioningBy

java
// 最多可以 分为两组——true是一组,false是一组。
Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));

Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =  menu.stream().collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));

Collectors 类的静态工厂方法

工厂方法返回类型用于示例
toListList<T>把流中所有项目收集到一个 ListList<Dish> dishes = menuStream.collect(toList());
toSetSet<T>把流中所有项目收集到一个 Set,删除重复项Set<Dish> dishes = menuStream.collect(toSet());
toCollectionCollection<T>把流中所有项目收集到给定的供应源创建的集合Collection<Dish> dishes = menuStream.collect(toCollection(), ArrayList::new);
countingLong计算流中元素的个数long howManyDishes = menuStream.collect(counting());
summingIntInteger对流中项目的一个整数属性求和int totalCalories = menuStream.collect(summingInt(Dish::getCalories));
averagingIntDouble计算流中项目 Integer 属性的平均值double avgCalories = menuStream.collect(averagingInt(Dish::getCalories));
joiningString连接对流中每个项目调用 toString 方法所生成的字符串String shortMenu = menuStream.map(Dish::getName).collect(joining(", "));
maxByOptional<T>一个包裹了流中按照给定比较器选出的最大元素的 Optional, 或如果流为空则为 Optional.empty()Optional<Dish> fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories)));
minByOptional<T>一个包裹了流中按照给定比较器选出的最小元素的 Optional, 或如果流为空则为 Optional.empty()Optional<Dish> lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories)));
reducing归约操作产生的类型从一个作为累加器的初始值开始,利用 BinaryOperator 与流 中的元素逐个结合,从而将流归约为单个值int totalCalories = menuStream.collect(reducing(0, Dish::getCalories, Integer::sum));
collectingAndThen转换函数返回的类型包裹另一个收集器,对其结果应用转换函数int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size));
groupingByMap<K, List<T>>根据项目的一个属性的值对流中的项目作问组,并将属性值作 为结果 Map 的键Map<Dish.Type,List<Dish>> dishesByType = menuStream.collect(groupingBy(Dish::getType));
partitioningByMap<Boolean, List<T>>根据对流中每个项目应用谓词的结果来对项目进行分区Map<Boolean, List<Dish>> vegetarianDishes = menuStream.collect(partitioningBy(Dish::isVegetarian));

并行数据处理与性能

java
// 1 + 2 + 3 + ...... + n
public static long parallelSum(long n) {
    return Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
}

配置并行流使用的线程池

并行流内部默认使用了 ForkJoinPool,默认的线程数量为 CPU 的数量,由 Runtime.getRuntime().availableProcessors() 得到。

可以通过修改系统属性来更改: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12");

这是一个全局设置,因此会影响代码中所有的并行流。一般而言,让 ForkJoinPool 的大小等于处理器数量是个不错的默认值,建议不要修改它。

测试流性能

java
package com.mengweijin.learning.basic.lambda;

import java.util.stream.LongStream;
import java.util.stream.Stream;

/**
 * 1 + 2 + 3 + ...... + n
 * @author mengweijin
 */
public class ParallelSumTest {

    /**
     * 50000005000000 =======> 559ms
     * 50000005000000 =======> 3800ms
     * 50000005000000 =======> 376ms
     * 50000005000000 =======> 30ms
     * @param args
     */
    public static void main(String[] args) {
        long n = 10000000;

        long start = System.currentTimeMillis();
        long sum = sequentialSum(n);
        long end = System.currentTimeMillis();
        System.out.println(sum + " =======> " + (end - start) + "ms");

        // 拆装箱影响性能
        start = System.currentTimeMillis();
        sum = parallelSum(n);
        end = System.currentTimeMillis();
        System.out.println(sum + " =======> " + (end - start) + "ms");

        // 避免拆装箱
        start = System.currentTimeMillis();
        sum = longStreamParallelSum(n);
        end = System.currentTimeMillis();
        System.out.println(sum + " =======> " + (end - start) + "ms");

        // iterate 影响性能,下面这个性能最佳
        start = System.currentTimeMillis();
        sum = longStreamParallelRangedSum(n);
        end = System.currentTimeMillis();
        System.out.println(sum + " =======> " + (end - start) + "ms");
    }

    public static long sequentialSum(long n) {
        return Stream.iterate(1L, i -> i + 1).limit(n).reduce(0L, Long::sum);
    }

    public static long parallelSum(long n) {
        return Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
    }

    public static long longStreamParallelSum(long n) {
        return LongStream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
    }

    public static long longStreamParallelRangedSum(long n) {
        return LongStream.rangeClosed(1, n).parallel().reduce(0L, Long::sum);
    }
}

高效使用并行流

  • 如有疑问,测量。把顺序流转成并行流轻而易举,但却不一定是好事。
  • 留意装箱。自动装箱和拆箱操作会大大降低性能。
  • 有些操作本身在并行流上的性能就比顺序流差。特别是 limit 和 findFirst 等依赖于元素顺序的操作。
  • 要考虑流的操作流水线的总计算成本。设 N 是元素总数,Q 是一个元素通过流水线的处理成本,则 N * Q 就是总成本。Q 值较高,就意味着使用并行流时性能好的可能性比较大。
  • 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。
  • 要考虑流背后的数据结构是否易于分解。例如,ArrayList 的拆分效率比 LinkedList 高的多。
  • 流自身的特点以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
  • 要考虑终端操作中合并步骤的代价是大是小。

流的数据源和可分解性

可分解性
ArrayList极佳
LinkedList
IntStream.range极佳
Stream.iterate
HashSet
TreeSet

分支/合并框架(Fork/Join)

TIP

if(任务足够小或不可分) { 顺序执行该任务 } else { 将任务分成两个子任务、 递归调用本方法,拆分每个子任务,等待所有子任务完成 合并每个子任务的结果 }

使用分支/合并框架的最佳做法

  • 对一个任务调用 join 方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子 任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂, 因为每个子任务都必须等待另一个子任务完成才能启动。
  • 不应该在 RecursiveTask 内部使用 ForkJoinPool 的 invoke 方法。相反,你应该始终直接调用 compute 或 fork 方法,只有顺序代码才应该用 invoke 来启动并行计算。
  • 对子任务调用 fork 方法可以把它排进 ForkJoinPool。同时对左边和右边的子任务调用 它似乎很自然,但这样做的效率要比直接对其中一个调用 compute 低。这样做你可以为 其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
  • 调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的 IDE 里面 看栈跟踪(stack trace)来找问题,但放在分支  合并计算上就不行了,因为调用 compute 的线程并不是概念上的调用方,后者是调用 fork 的那个。
  • 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计算快。

高效 Java 8 编程

改善代码可读性

  • 重构代码,用 Lambda 表达式取代匿名类
  • 用方法引用重构 Lambda 表达式
  • 用 Stream API 重构命令式的数据处理
java
// 1. 重构代码,用 Lambda 表达式取代匿名类
Runnable r1 = new Runnable() {
    public void run(){
        System.out.println("Hello");
    }
};

Runnable r2 = () ->  System.out.println("Hello");

// 2. 用方法引用重构 Lambda 表达式
int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

// 3. 用 Stream API 重构命令式的数据处理
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){
    if(dish.getCalories() > 300){
        dishNames.add(dish.getName());
    }
}

menu.parallelStream()
    .filter(d -> d.getCalories() > 300)
    .map(Dish::getName)
    .collect(toList());

使用 Lambda 重构面向对象的设计模式

策略模式

java
public interface ValidationStrategy {
    boolean execute(String s);
}
public class IsAllLowerCase implements ValidationStrategy {
    public boolean execute(String s){
        return s.matches("[a-z]+");
    }
}
public class IsNumeric implements ValidationStrategy {
    public boolean execute(String s){
        return s.matches("\\d+");
    }
}

public class Validator {
    private final ValidationStrategy strategy;
    public Validator(ValidationStrategy v){
        this.strategy = v;
    }
    public boolean validate(String s){
        return strategy.execute(s);
    }
}

Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaaa");

Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("bbbb");
java
// 使用 Lambda 表达式
// 通过直接传递Lambda表达式就能达到同样的目的,并且还更简洁
 Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
 boolean b1 = numericValidator.validate("aaaa");

 Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
 boolean b2 = lowerCaseValidator.validate("bbbb");

模板方法模式

java
abstract class OnlineBanking {
    public void processCustomer(int id){
        Customer c = Database.getCustomerWithId(id);
        makeCustomerHappy(c);
    }

    abstract void makeCustomerHappy(Customer c);
}
java
// 使用 Lambda 表达式
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
    Customer c = Database.getCustomerWithId(id);
    makeCustomerHappy.accept(c);
}

// 现在,你可以很方便地通过传递Lambda表达式,直接插入不同的行为,不再需要继承 OnlineBanking 类了:
new OnlineBankingLambda().processCustomer(1337,
    (Customer c) -> System.out.println("Hello " + c.getName());

观察者模式

java
interface Observer {
    void notify(String tweet);
}

class NYTimes implements Observer{
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("money")){
            System.out.println("Breaking news in NY! " + tweet);
        }
    }
}
class Guardian implements Observer{
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("queen")){
            System.out.println("Yet another news in London... " + tweet);
        }
    }
}
class LeMonde implements Observer{
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("wine")){
            System.out.println("Today cheese, wine and news! " + tweet);
        }
    }
}

interface Subject{
    void registerObserver(Observer o);
    void notifyObservers(String tweet);
}

class Feed implements Subject{
    private final List<Observer> observers = new ArrayList<>();
    public void registerObserver(Observer o) {
        this.observers.add(o);
    }
    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}

Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Java 8 in Action!");
java
// 使用 Lambda 表达式
f.registerObserver((String tweet) -> {
    if(tweet != null && tweet.contains("money")){
        System.out.println("Breaking news in NY! " + tweet);
    }
});
f.registerObserver((String tweet) -> {
    if(tweet != null && tweet.contains("queen")){
        System.out.println("Yet another news in London... " + tweet);
    }
});

是否我们随时随地都可以使用 Lambda 表达式呢?

答案是否定的!我们前文介绍的例子中,Lambda 适配得很好,那是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,诸如此类。在这些情形下,你还是应该继续使用类的方式。

责任链模式

java
public abstract class ProcessingObject<T> {
    protected ProcessingObject<T> successor;
    public void setSuccessor(ProcessingObject<T> successor){
        this.successor = successor;
    }
    public T handle(T input){
        T r = handleWork(input);
        if(successor != null){
            return successor.handle(r);
        }
        return r;
    }

    abstract protected T handleWork(T input);
}

public class HeaderTextProcessing extends ProcessingObject<String> {
    public String handleWork(String text){
        return "From Raoul, Mario and Alan: " + text;
    }
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
    public String handleWork(String text){
        return text.replaceAll("labda", "lambda");
    }
}

ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle("Aren't labdas really sexy?!!");
// 打印输出“From Raoul, Mario and Alan: Aren't lambdas really sexy?!!”
System.out.println(result);
java
// 使用 Lambda 表达式
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!");

工厂模式

java
public class ProductFactory {
    public static Product createProduct(String name){
        switch(name){
            case "loan": return new Loan();
            case "stock": return new Stock();
            case "bond": return new Bond();
            default: throw new RuntimeException("No such product " + name);
        }
    }
}

Product p = ProductFactory.createProduct("loan");
java
// 使用 Lambda 表达式
Supplier<Product> loanSupplier = Loan::new;
Loan loan = loanSupplier.get();

final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
    map.put("loan", Loan::new);
    map.put("stock", Stock::new);
    map.put("bond", Bond::new);
}
public static Product createProduct(String name){
    Supplier<Product> p = map.get(name);
    if(p != null) return p.get();
    throw new IllegalArgumentException("No such product " + name);
}
Product p = ProductFactory.createProduct("loan");

Lambda 调试

peek

peek 的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。

java
List<Integer> result = numbers.stream()
    .peek(x -> System.out.println("from stream: " + x))
    .map(x -> x + 17)
    .peek(x -> System.out.println("after map: " + x))
    .filter(x -> x % 2 == 0)
    .peek(x -> System.out.println("after filter: " + x))
    .limit(3)
    .peek(x -> System.out.println("after limit: " + x))
    .collect(toList());

通过peek操作我们能清楚地了解流水线操作中每一步的输出结果:

from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22
after filter: 22
after limit: 22

默认方法 default 关键词

java
public interface Sized {
    int size();
    default boolean isEmpty() {
        return size() == 0;
    }
}

Java 8 中的抽象类和抽象接口的区别

它们不都能包含抽象方法和包含方法体的实现吗? 首先,一个类只能继承一个抽象类,但是一个类可以实现多个接口。 其次,一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。

默认方法的使用模式

  1. 可选方法 在 Java 8 中,Iterator 接口就为 remove 方法提供了 一个默认实现,如下所示:
java
interface Iterator<T> {
   boolean hasNext();
   T next();
   default void remove() {
       throw new UnsupportedOperationException();
   }
}

通过这种方式,你可以减少无效的模板代码。实现 Iterator 接口的每一个类都不需要再声 明一个空的 remove 方法了,因为它现在已经有一个默认的实现。

  1. 行为的多继承 Java 的类只能继承单一的类,但是一个类可以实现多接口。如果实现的多个接口都有各自的默认实现方法,那么就间接的实现了类的多继承。

  2. 解决多继承冲突的规则 我们知道 Java 语言中一个类只能继承一个父类,但是一个类可以实现多个接口。随着默认方法在 Java 8 中引入,有可能出现一个类继承了多个方法而它们使用的却是同样的函数签名。这种情况下,类会选择使用哪一个函数?

解决问题的三条规则:

  • (1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
  • (2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果 B 继承了 A,那么 B 就比 A 更加具体。
  • (3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。

用 Optional 取代 null

采用防御式检查减少 NullPointerException

  1. 深层质疑
java
public String getCarInsuranceName(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}

每次你不确定一个变量是否为 null 时,都需要添加一个进一步嵌套的 if 块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还牺牲了代码的可读性。

  1. 过多的退出语句
java
public String getCarInsuranceName(Person person) {
    if (person == null) {
        return "Unknown";
    }
    Car car = person.getCar();
    if (car == null) {
        return "Unknown";
    }
    Insurance insurance = car.getInsurance();
    if (insurance == null) {
        return "Unknown";
    }
    return insurance.getName();
}

这种方案远非理想,现在这个方法有了四个截然不同的退出点,使得代码的维护异常艰难。

null 带来的种种问题

  • 它是错误之源。 NullPointerException 是目前 Java 程序开发中最典型的异常。
  • 它会使你的代码膨胀。它让你的代码充斥着深度嵌套的 null 检查,代码的可读性糟糕透顶。
  • 它自身是毫无意义的。null 自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对 缺失变量值的建模。
  • 它破坏了 Java 的哲学。Java 一直试图避免让程序员意识到指针的存在,唯一的例外是:null 指针。
  • 它在 Java 的类型系统上开了个口子。null 并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个 null 变量最初的 赋值到底是什么类型。

Optional 类入门

java
public class Person {
    private Optional<Car> car;
    public Optional<Car> getCar() {
        return car;
    }
}
public class Car {
    private Optional<Insurance> insurance;
    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}
public class Insurance {
    private String name;
    public String getName() {
        return name;
    }
}

Optional<T> 类 —— 来避免出现 NullPointer 异常

方法描述示例
empty()返回一个空的 Optional 实例。
of(T)将指定值用 Optional 封装之后返回,如果该值为 null,则抛出一个 NullPointerException 异常
ofNullable(T)将指定值用 Optional 封装之后返回,如果该值为 null,则返回一个空的 Optional 对象
isPresent()将在 Optional 包含值的时候返回 true, 否则返回 false。
ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码块。Consumer 函数式接口:它让你传递一个接收 T 类型参数,并返回 void 的 Lambda 表达式。
T get()会在值存在时返回值,否则抛出一个 NoSuchElement 异常。
T orElse(T other)会在值存在时返回值,否则返回一个默认值。Optional<Integer> = Optional.of(0);
int value = optional.orElse(1);
T orElseGet(Supplier<? extends T> other)是 orElse 方法的延迟调用版,Supplier 方法只有在 Optional 对象不含值时才执行调用。
T orElseThrow(Supplier<? extends X> exceptionSupplier)遭遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow 你可以定制希望抛出的异常类型。
filter()剔除特定的值,如果值存在并且满足提供的谓词,就返回包含该值的 Optional 对象;否则返回一个空的 Optional 对象
map()如果值存在,就对该值执行提供的 mapping 函数调用
flatMap()如果值存在,就对该值执行提供的 mapping 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象

创建 Optional 对象

  1. 声明一个空的 Optional 对象
java
Optional<Car> optCar = Optional.empty();
  1. 创建一个非空值的 Optional 如果 car 是一个 null,这段代码会立即抛出一个 NullPointerException,而不是等到你 试图访问 car 的属性值时才返回一个错误。
java
Optional<Car> optCar = Optional.of(car);
  1. 可接受 null 的 Optional 如果 car 是 null,那么得到的 Optional 对象就是个空对象。
java
Optional<Car> optCar = Optional.ofNullable(car);

使用 map 从 Optional 对象中提取和转换值

java
String name = null;
if(insurance != null){
    name = insurance.getName();
}

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

如果 Optional 包含一个值,那函数就将该值作为参数传递给 map,对该值进行转换。如果 Optional 为空,就什么也不做。

使用 flatMap 链接 Optional 对象

java
public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}

// 不幸的是,下面这段代码无法通过编译。map操作的结果是一个Optional<Optional<Car>>类型的对象,遭遇的嵌套式 optional 结构。
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson
    .map(Person::getCar)
    .map(Car::getInsurance)
    .map(Insurance::getName);

flagMap 会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化为一个单一的流。这里你希望的结果其实也是类似的,但是你想要的是将两层的 optional 合并为一个。

java
public String getCarInsuranceName(Optional<Person> person) {
    return person.flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown");
}

使用 filter 剔除特定的值

java
Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
    System.out.println("ok");
}

Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -> System.out.println("ok"));

使用 Optional 的实战示例

用 Optional 封装可能为 null 的值

java
Optional<Object> value = Optional.ofNullable(map.get("key"));

异常与 Optional 的对比

java
public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

基础类型的 Optional 对象,以及为什么应该避免使用它们

Optional 也提供了类似的基础类型——OptionalInt、OptionalLong 以及 OptionalDouble。 不推荐大家使用基础类型的 Optional,因为基础类型的 Optional 不支持 map、如果 String 能转换为对应的 Integer,将其封装在 Optioal 对象中返回否则返回一个空的 Optional 对象 10.4 使用 Optional 的实战示例 flatMap 以及 filter 方法,而这些却是 Optional 类最有用的方法。

CompletableFuture:组合式异步编程

Future 接口

java
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对象完成之前超过已过期
}

Future 接口的局限性

  • 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第 一个的结果。
  • 等待 Future 集合中的所有任务都完成。
  • 仅等待 Future 集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同 一个值),并返回它的结果。
  • 通过编程方式完成一个 Future 任务的执行(即以手工设定异步操作结果的方式)。
  • 应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future 计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)。

使用 CompletableFuture 构建异步应用

CompletableFuture 具有一定的优势,因为它允许你对执行器(Executor)进行配置,尤其是线程池的大小,而这是并行流 Stream API 无法提供的。

没有指定 Executor 的方法会使用 ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。

CompletableFuture 创建 —— runAsync 和 supplyAsync

  • supplyAsync 可以支持返回值。
  • runAsync 方法不支持返回值。
java
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

//无返回值
public static void runAsync() throws Exception {
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
        System.out.println("run end ...");
    });

    future.get();
}

//有返回值
public static void supplyAsync() throws Exception {
    CompletableFuture<Long> future = CompletableFuture.supplyAsync(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
        System.out.println("run end ...");
        return System.currentTimeMillis();
    });

    long time = future.get();
    System.out.println("time = "+time);
}

回调函数(callback)

java
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)

可以看到 Action 的类型是 BiConsumer<? super T,? super Throwable>它可以处理正常的计算结果,或者异常情况。

whenComplete 和 whenCompleteAsync 的区别:

  • whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
  • whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
java
public static void whenComplete() throws Exception {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            if(new Random().nextInt()%2>=0) {
                int i = 12/0;
            }
            System.out.println("run end ...");
        });

        future.whenComplete((t, action) -> System.out.println("执行完成!"));

        future.exceptionally(t -> {
            System.out.println("执行失败!"+t.getMessage());
            return null;
        });

        TimeUnit.SECONDS.sleep(2);
    }

thenApply

当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。

java
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

Function<? super T,? extends U>

  • T:上一个任务返回结果的类型
  • U:当前任务的返回值类型
java
private static void thenApply() throws Exception {
    CompletableFuture<Long> future = CompletableFuture.supplyAsync(() -> {
        long result = new Random().nextInt(100);
        System.out.println("result1=" + result);
        return result;
    }).thenApply(t -> {
        long result = t * 5;
        System.out.println("result2=" + result);
        return result;
    });

    long result = future.get();
    System.out.println(result);
}

handle

handle 是执行任务完成时对结果的处理。

handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行,还可以处理异常的任务。thenApply 只可以执行正常的任务,任务出现异常则不执行 thenApply 方法。

java
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);
java
public static void handle() throws Exception {
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        int i = 10 / 0;
        return new Random().nextInt(10);
    }).handle((param, throwable) -> {
        int result = -1;
        if (throwable == null) {
            result = param * 2;
        } else {
            System.out.println(throwable.getMessage());
        }
        return result;
    });
    System.out.println(future.get());
}

从示例中可以看出,在 handle 中可以根据任务是否有异常来进行做相应的后续处理操作。而 thenApply 方法,如果上个任务出现错误,则不会执行 thenApply 方法。

thenAccept 消费处理结果

接收任务的处理结果,并消费处理,无返回结果。

java
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);
java
public static void thenAccept() throws Exception {
    CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> new Random().nextInt(10))
        .thenAccept(System.out::println);
    future.get();
}

该方法只是消费执行完成的任务,并可以根据上面的任务返回的结果进行处理。并没有后续的输错操作。

thenRun

跟 thenAccept 方法不一样的是,不关心任务的处理结果。只要上面的任务执行完成,就开始执行 thenRun。

  • thenAccept 接收上一阶段的输出作为本阶段的输入
  • thenRun 根本不关心前一阶段的输出,根本不不关心前一阶段的计算结果,因为它不需要输入参数
java
public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);
java
public static void thenRun() throws Exception {
    CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> new Random().nextInt(10))
        .thenRun(() -> System.out.println("thenRun ..."));
    future.get();
}

thenCombine 整合两个计算结果

java
public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);


private static void thenCombine() throws Exception {
    CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "hello");
    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "hello");
    CompletableFuture<String> result = future1.thenCombine(future2, (t, u) -> t + " " + u);
    System.out.println(result.get());
}

thenAcceptBoth

当两个 CompletionStage 都执行完成后,把结果一块交给 thenAcceptBoth 来进行消耗

java
public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action,     Executor executor);


private static void thenAcceptBoth() throws Exception {
    CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        System.out.println("f1=" + t);
        return t;
    });

    CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        System.out.println("f2=" + t);
        return t;
    });
    f1.thenAcceptBoth(f2, (t, u) -> System.out.println("f1=" + t + ";f2=" + u + ";"));
}

applyToEither

两个 CompletionStage,谁执行返回的结果快,我就用那个 CompletionStage 的结果进行下一步的转化操作。

java
public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn,Executor executor);


private static void applyToEither() throws Exception {
    CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f1=" + t);
        return t;
    });
    CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f2=" + t);
        return t;
    });

    CompletableFuture<Integer> result = f1.applyToEither(f2, t -> {
        System.out.println(t);
        return t * 2;
    });

    System.out.println(result.get());
}

acceptEither 方法

两个 CompletionStage,谁执行返回的结果快,我就用那个 CompletionStage 的结果进行下一步的消耗操作。

java
public CompletionStage<Void> acceptEither(CompletionStage<? extends T> other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action,Executor executor);


private static void acceptEither() throws Exception {
    CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f1="+t);
        return t;
    });

    CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f2="+t);
        return t;
    });
    f1.acceptEither(f2, System.out::println);
}

runAfterEither

两个 CompletionStage,任何一个完成了都会执行下一步的操作(Runnable)

java
public CompletionStage<Void> runAfterEither(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor);


private static void runAfterEither() throws Exception {
    CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f1="+t);
        return t;
    });

    CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f2="+t);
        return t;
    });
    f1.runAfterEither(f2, () -> System.out.println("上面有一个已经完成了。"));
}

runAfterBoth

两个 CompletionStage,都完成了计算才会执行下一步的操作(Runnable)

java
public CompletionStage<Void> runAfterBoth(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor);


private static void runAfterBoth() throws Exception {
    CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f1="+t);
        return t;
    });

    CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        try {
            TimeUnit.SECONDS.sleep(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("f2="+t);
        return t;
    });
    f1.runAfterBoth(f2, () -> System.out.println("上面两个任务都执行完成了。"));
}

thenCompose

thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。

java
public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ;
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) ;


private static void thenCompose() throws Exception {
    CompletableFuture<Integer> f = CompletableFuture.supplyAsync(() -> {
        int t = new Random().nextInt(3);
        System.out.println("t1=" + t);
        return t;
    }).thenCompose(param -> CompletableFuture.supplyAsync(() -> {
        int t = param * 2;
        System.out.println("t2=" + t);
        return t;
    }));
    System.out.println("thenCompose result : " + f.get());
}

并行——使用流还是 CompletableFutures?

对集合进行并行计算有两种方式:要么将其转化为并行流,利用 map 这样的操作开展工作,要么枚举出集合中的每一个元素,创建新的线程,在 CompletableFuture 内对其进行操作。后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助你确保整体的计算不会因为线程都在等待 I/O 而发生阻塞。

我们对使用这些 API 的建议如下。

  1. 如果你进行的是计算密集型的操作,并且没有 I/O,那么推荐使用 Stream 接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程)。
  2. 反之,如果你并行的工作单元还涉及等待 I/O 的操作(包括网络连接等待),那么使用 CompletableFuture 灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者 W/C 的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生 I/O 等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。

新的日期和时间 API

LocalDate, LocalTime, LocalDateTime 都不带时区信息。

LocalDate、LocalTime 和 LocalDateTime

java
LocalDate date = LocalDate.of(2014, 3, 18);  // 2014-03-18
int year = date.getYear();  // 2014
Month month = date.getMonth(); // MARCH
int day = date.getDayOfMonth();  // 18
DayOfWeek dow = date.getDayOfWeek();  // TUESDAY
int len = date.lengthOfMonth(); // 31(days in March)
boolean leap = date.isLeapYear(); // false(不是闰年)

// 系统当前日期
LocalDate today = LocalDate.now();

// 使用 TemporalField 读取 LocalDate 的值
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

// LocalTime
LocalTime time = LocalTime.of(13, 45, 20);  // 13:45:20
int hour = time.getHour();  // 13
int minute = time.getMinute();  // 45
int second = time.getSecond(); // 20

// parse
LocalDate date = LocalDate.parse("2014-03-18");
LocalTime time = LocalTime.parse("13:45:20");

// LocalDateTime
// 2014-03-18T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, 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);

LocalDate date1 = dt1.toLocalDate();  // 2014-03-18
LocalTime time1 = dt1.toLocalTime();    // 13:45:20

Instant 机器的日期和时间格式

作为人,我们习惯于以星期几、几号、几点、几分这样的方式理解日期和时间。毫无疑问,这种方式对于计算机而言并不容易理解。从计算机的角度来看,建模时间最自然的格式是表示一个持续时间段上某个点的单一大整型数。这也是新的 java.time.Instant 类对时间建模的方式,基本上它是以 Unix 元年时间(传统的设定为 UTC 时区 1970 年 1 月 1 日午夜时分)开始所经历的秒数进行计算。

Instant 的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些我们非常容易理解的时间单位。比如下面这段语句:

int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

它会抛出下面这样的异常: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth

但是你可以通过 Duration 和 Period 类使用 Instant。

Duration 和 Period

java
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);

由于 LocalDateTime 和 Instant 是为不同的目的而设计的,一个是为了便于人阅读使用, 另一个是为了便于机器处理,所以你不能将二者混用。如果你试图在这两类对象之间创建 duration,会触发一个 DateTimeException 异常。此外,由于 Duration 类主要用于以秒和纳秒衡量时间的长短,你不能仅向 between 方法传递一个 LocalDate 对象做参数。

如果你需要以年、月或者日的方式对多个时间单位建模,可以使用 Period 类。

java
Period tenDays = Period.between(LocalDate.of(2014, 3, 8),
                                LocalDate.of(2014, 3, 18));
java
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

日期 —— 时间类中表示时间间隔的通用方法

方法名是否是静态方法方法描述
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

操纵、解析和格式化日期

如果你已经有一个 LocalDate 对象,想要创建它的一个修改版,最简单的方法是使用 withAttribute 方法。withAttribute 方法会创建对象的一个副本,并按照需要修改它的属性。注意,它们都不会修改原来的对象!

java
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

表示时间点的日期  时间类的通用方法

方法名是否是静态方法方法描述
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 对象为模板,对某些状态进行修改创建该对象的副本

使用 TemporalAdjuster

场景:将日期调整到下个周日、下个工作日,或者是本月的最后一天。

java
 import static java.time.temporal.TemporalAdjusters.*;

 LocalDate date1 = LocalDate.of(2014, 3, 18);  //  2014-03-18
 LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));  //  2014-03-23
 LocalDate date3 = date2.with(lastDayOfMonth()); //  2014-03-31

TemporalAdjuster 类中的工厂方法

方法名方法描述
dayOfWeekInMonth创建一个新的日期,它的值为同一个月中每一周的第几天
firstDayOfMonth创建一个新的日期,它的值为当月的第一天
firstDayOfNextMonth创建一个新的日期,它的值为下月的第一天
firstDayOfNextYear创建一个新的日期,它的值为明年的第一天
firstDayOfYear创建一个新的日期,它的值为当年的第一天
firstInMonth创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值
lastDayOfMonth创建一个新的日期,它的值为当月的最后一天
lastDayOfNextMonth创建一个新的日期,它的值为下月的最后一天
lastDayOfNextYear创建一个新的日期,它的值为明年的最后一天
lastDayOfYear创建一个新的日期,它的值为今年的最后一天
lastInMonth创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值
next/previous创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期
nextOrSame/previousOrSame创建一个新的日期,并将其值设定为日期调整后或者调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象

DateTimeFormatter

java
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);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);   // 18/03/2014
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

//  创建一个本地化的DateTimeFormatter
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date.format(italianFormatter); // 18. marzo 2014
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

DateTimeFormatterBuilder 更加细粒度的控制

java
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
    .appendText(ChronoField.DAY_OF_MONTH)
    .appendLiteral(". ")
    .appendText(ChronoField.MONTH_OF_YEAR)
    .appendLiteral(" ")
    .appendText(ChronoField.YEAR)
    .parseCaseInsensitive()
    .toFormatter(Locale.ITALIAN);

处理不同的时区和历法

java
//调用ZoneId的getRules()得到指定时区的规则。
ZoneRules zoneRules = ZoneId.getRules();
ZoneId romeZone = ZoneId.of("Europe/Rome");

地区 ID 都为“{区域}/{城市}”的格式,这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。你可以通过 Java 8 的新方法 toZoneId 将一个老的时区对象转换为 ZoneId:

java
ZoneId zoneId = TimeZone.getDefault().toZoneId();

为时间点添加时区信息

java
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);

// 通过ZoneId,你还可以将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);

// 你也可以通过反向的方式得到LocalDateTime对象:
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

利用和 UTC/格林尼治时间的固定偏差计算时区

java
// 纽约落后于伦敦5小时
// “-05:00”的偏差实际上对应的是美国东部标准时间。
// 注意,使用这种方式定义的ZoneOffset 并未考虑任何日光时的影响,所以在大多数情况下,不推荐使用。
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date, newYorkOffset);

超越 Java 8

递归和迭代

阶乘计算的三种方式

java
// 迭代
static int factorialIterative(int n) {
    int r = 1;
    for (int i = 1; i <= n; i++) {
        r *= i;
    }
    return r;
}

// 递归。每次执行都会在调用栈上创建一个新的栈帧,容易遭遇StackOverflowError,效率差,因此要多避免保存每次递归的中间值
static long factorialRecursive(long n) {
    return n == 1 ? 1 : n * factorialRecursive(n-1);
}

// Stream
static long factorialStreams(long n){
    return LongStream.rangeClosed(1, n)
        .reduce(1, (long a, long b) -> a * b);
}

建议,你应该尽量使用 Stream 取代迭代操作,从而避免变化带来的影响。此外,如果递归能让你以更精炼,并且不带任何副作用的方式实现算法,你就应该用递归替换迭代。

高阶函数

  • 接受至少一个函数作为参数
  • 返回的结果是一个函数

科里化

科里化 ① 是一种将具备 2 个参数(比如,x 和 y)的函数 f 转化为使用一个参数的函数 g,并且这个函数的返回值也是一个函数,它会作为新函数的一个参数。后者的返回值和初始函数的返回值相同,即 f(x,y) = (g(x))(y)。

当然,我们可以由此推出:你可以将一个使用了 6 个参数的函数科里化成一个接受第 2、4、6 号参数,并返回一个接受 5 号参数的函数,这个函数又返回一个接受剩下的第 1 号和第 3 号参数的函数。

一个函数使用所有参数仅有部分被传递时,通常我们说这个函数是部分应用的(partially applied)。

其他语言特性的更新

重复注解

java
// 之前版本的Java禁止对同样的注解类型声明多次。由于这个原因,下面的第二句代码是无效的:
// 有效的
@interface Author { String name(); }

// 无效的多个 @Author 注解
@Author(name="Raoul") @Author(name="Mario") @Author(name="Alan")  class Book{ }

Java 企业版的程序员经常通过一些惯用法绕过这一限制。你可以声明一个新的注解,它包含了你希望重复的注解数组。

java
@interface Author { String name(); }

@interface Authors {   Author[] value();  }

@Authors(
    { @Author(name="Raoul"), @Author(name="Mario") , @Author(name="Alan")}
)
class Book{}

Java 8 想要从根本上移除这一限制的原因,去掉这一限制后,代码的可读性会好很多。现在,如果你的配置允许重复注解,你可以毫无顾虑地一次声明多个同一种类型的注解。它目前还不是默认行为,你需要显式地要求进行重复注解。

创建一个重复注解

如果一个注解在设计之初就是可重复的,你可以直接使用它。但是,如果你提供的注解是为用户提供的,那么就需要做一些工作,说明该注解可以重复。下面是你需要执行的两个步骤:

  1. 将注解标记为@Repeatable
  2. 提供一个注解的容器

下面的例子展示了如何将@Author 注解修改为可重复注解:

java
@Repeatable(Authors.class)
@interface Author { String name(); }
@interface Authors {   Author[] value();  }

// 完成了这样的定义之后,Book类可以通过多个 @Author注解进行注释,如下所示:
@Author(name="Raoul") @Author(name="Mario") @Author(name="Alan")
class Book{ }

类 Class 提供了一个新的 getAnnotationsByType 方法,它可以帮助我们更好地使用重复注解。

比如,你可以像下面这样打印输出 Book 类的所有 Author 注解:

java
public static void main(String[] args) {
    Author[] authors = Book.class.getAnnotationsByType(Author.class);
    Arrays.asList(authors).forEach(a -> { System.out.println(a.name()); });
}

类型注解

从 Java 8 开始,注解已经能应用于任何类型。这其中包括 new 操作符、类型转换、instanceof 检查、泛型类型参数,以及 implements 和 throws 子句。

java
@NonNull String name = person.getName();

// 类似地,你可以对列表中的元素类型进行注解:
List<@NonNull Car> cars = new ArrayList<>();

Checker 注解检查框架

Java 8 并未提供官方的注解或者一种工具能以开箱即用的方式使用它们。它仅仅提供了一种功能,你使用它可以对不同的类型添加注解。幸运的是,这个世界上还存在一个名为 Checker 的框架,它定义了多种类型注解,使用它们你可以增强类型检查。如果对此感兴趣,我们建议你看看它的教程,地址链接为:http://www.checker-framework.org。关于在代码中的何处使用注解的更多内容,可以访问http://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.7.4。

目标类型推断

java
List<Car> cars = Collections.<Car>emptyList();
// 等价于下面
List<Car> cars = Collections.emptyList();

类库的更新

集合中的新增方法

类/接口新方法
MapgetOrDefault,forEach,compute,computeIfAbsent,computeIfPresent,merge, putIfAbsent,remove(key,value),replace,replaceAll
IterableforEach,spliterator
IteratorforEachRemaining
CollectionremoveIf,stream,parallelStream
ListreplaceAll,sort
BitSetstream

Map.getOrDefault() 默认值

java
// Java 8 以前:
Map<String, Integer> carInventory = new HashMap<>();
Integer count = 0;
if(map.containsKey("Aston Martin")){
    count = map.get("Aston Martin");
}

// Java 8:
Integer count = map.getOrDefault("Aston Martin", 0);

Map.computeIfAbsent() 缓存模式

帮助你非常方便地使用缓存模式。比如,我们假设你需要从不同的网站抓取和处理数据。这种场景下,如果能够缓存数据是非常有帮助的,这样你就不需要每次都执行(代价极高的)数据抓取操作了。

java
// Java 8 以前:
Object value = map.get("key");
if(key == null) {
    // 新创建一个对象,也可以是一个函数来进行复杂的业务处理后,返回一个值。
    key = new Object();
    map.put("key", value);
}

// Java 8:
Object value = map.computeIfAbsent("key", vlue -> new Object());

Collection.removeIf() 移除集合中满足某个谓词的所有元素。

List.replaceAll()

replaceAll 方法会对列表中的每一个元素执行特定的操作,并用处理的结果替换该元素。它的功能和 Stream 中的 map 方法非常相似,不过 replaceAll 会修改列表中的元素。

java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.replaceAll(x -> x * 2);
// 打印输出:[2,4,6,8,10]
System.out.println(numbers);

Comparator 类

Comparator 接口现在同时包含了默认方法和静态方法。

新的实例方法包含了下面这些。

  • reversed—— 对当前的 Comparator 对象进行逆序排序,并返回排序之后新的 Comparator 对象。
  • thenComparing——当两个对象相同时,返回使用另一个 Comparator 进行比较的 Comparator 对象。
  • thenComparingInt、thenComparingDouble、thenComparingLong——这些方法的工作方式和 thenComparing 方法类似,不过它们的处理函数是特别针对某些基本数据类型(分别对应于 ToIntFunction、ToDoubleFunction 和 ToLongFunction)的。

新的静态方法包括下面这些。

  • Comparator.comparing 返回一个 Comparator 对象,该对象提供了一个函数可以提取排序关键字。
  • comparingInt、comparingDouble、comparingLong——它们的工作方式和 comparing 类似,但接受的函数特别针对某些基本数据类型(分别对应于 ToIntFunction、ToDoubleFunction 和 ToLongFunction)。
  • naturalOrder——对 Comparable 对象进行自然排序,返回一个 Comparator 对象。
  • nullsFirst、nullsLast——对空对象和非空对象进行比较,你可以指定空对象(null)比非空对象(non-null)小或者比非空对象大,返回值是一个 Comparator 对象。
  • reverseOrder——和 naturalOrder().reversed()方法类似。

并发

原子操作

java.util.concurrent.atomic 包提供了多个对数字类型进行操作的类,比如 AtomicInteger 和 AtomicLong,它们支持对单一变量的原子操作。这些类在 Java 8 中新增了更多的方法支持。

  • getAndUpdate——以原子方式用给定的方法更新当前值,并返回变更之前的值。
  • updateAndGet——以原子方式用给定的方法更新当前值,并返回变更之后的值。
  • getAndAccumulate——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之前的值。
  • accumulateAndGet——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之后的值。

何以原子方式比较一个现存的原子整型值和一个给定的观测值(比如 10),并将变量设定为二者中较小的一个。

int min = atomicInteger.accumulateAndGet(10, Integer::min);

Adder 和 Accumulator

多线程的环境中,如果多个线程需要频繁地进行更新操作,且很少有读取的动作(比如,在统计计算的上下文中),Java API 文档中推荐大家使用新的类 LongAdder、LongAccumulator、 Double-Adder 以及 DoubleAccumulator,尽量避免使用它们对应的原子类型。这些新的类在设计之初就考虑了动态增长的需求,可以有效地减少线程间的竞争。

java
// 使用 LongAdder 计算多个值之和
// 使用默认构造器,初始值被设为 0
LongAdder adder = new LongAdder();
// 在多个不同的线程中进行加法运算
adder.add(10);
// …
long sum = adder.sum();


// 使用 LongAccumulator 计算多个值之和
LongAccumulator acc = new LongAccumulator(Long::sum, 0);
// 在几个不同的线程中累计计算值
acc.accumulate(10);
// …
long result = acc.get();

ConcurrentHashMap

  1. 性能。典型情况下,map 的 条目会被存储在桶中,依据键生成哈希值进行访问。但是,如果大量键返回相同的哈希值,由于桶是由 List 实现的,它的查询复杂度为 O(n),这种情况下性能会恶化。在 Java 8 中,当桶过于臃肿时,它们会被动态地替换为排序树(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 中的最大值:

java
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 无法表示的。

Arrays

  1. parallelSort。 parallelSort 方法会以并发的方式对指定的数组进行排序,你可以使用自然顺序,也可以为数组对象定义特别的 Comparator。

  2. setAll 和 parallelSetAll setAll 和 parallelSetAll 方法可以以顺序的方式也可以用并发的方式,使用提供的函数计算每一个元素的值,对指定数组中的所有元素进行设置。该函数接受元素的索引,返回该索引元素对应的值。由于 parallelSetAll 需要并发执行,所以提供的函数必须没有任何副作用,

举例来说,你可以使用 setAll 方法生成一个值为 0, 2, 4, 6, …的数组:

java
int[] evenNumbers = new int[10];
Arrays.setAll(evenNumbers, i -> i * 2);
  1. parallelPrefix parallelPrefix 方法以并发的方式,用用户提供的二进制操作符对给定数组中的每个元素进行累积计算。通过下面这段代码,你会得到这样的一些值:1, 2, 3, 4, 5, 6, 7, …。
java
int[] ones = new int[10];
Arrays.fill(ones, 1);
Arrays.parallelPrefix(ones, (a, b) -> a + b);
  1. 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 类型的值转换为对应的基础类型。不过,如果在转换过程中有信息的丢失,方法会抛出算术异常。
  1. Math 如果 Math 中的方法在操作中出现溢出,Math 类提供了新的方法可以抛出算术异常。支持这一异常的方法包括使用 int 和 long 参数的 addExact、subtractExact、multipleExact、incrementExact、decrementExact 和 negateExact。此外,Math 类还新增了一个静态方法 toIntExact,可以将 long 值转换为 int 值。其他的新增内容包括静态方法 floorMod、floorDiv 和 nextDown。

  2. Files Files 类最引人注目的改变是,你现在可以用文件直接产生流。 一些非常有用的静态方法可以返回流。

  • Files.lines——可以延迟方式读取文件的内容,并将其作为一个流。
  • Files.list——生成由指定目录中所有条目构成的 Stream<Path>。这个列表不是递归包含的。由于流是延迟消费的,处理包含内容非常庞大的目录时,这个方法非常有用。
  • Files.walk——和 Files.list 有些类似,它也生成包含给定目录中所有条目的 Stream<Path>。不过这个列表是递归的,你可以设定递归的深度。注意,该遍历是依照深度优先进行的。
  • Files.find——通过递归地遍历一个目录找到符合条件的条目,并生成一个 Stream<Path>对象。
  1. Reflection Relection 接口的另一个变化是新增了可以查询方法参数信息的 API,比如,你现在可以使用新增的 java.lang.reflect.Parameter 类查询方法参数的名称和修饰符,这个类被新的 java.lang.reflect.Executable 类所引用,而 java.lang.reflect.Executable 通用函数和构造函数共享的父类。

  2. String String 类也新增了一个静态方法,名叫 join。你大概已经猜出它的功能了,它可以用一个分隔符将多个字符串连接起来。你可以像下面这样使用它:

java
String authors = String.join(", ", "Raoul", "Mario", "Alan");
System.out.println(authors);

Lambda 表达式和 JVM 字节码

invokedynamic 指令

字节码指令 invokedynamic 最初被 JDK7 引入,用于支持运行于 JVM 上的动态类型语言。执行方法调用时,invokedynamic 添加了更高层的抽象,使得一部分逻辑可以依据动态语言的特征来决定调用目标。这一指令的典型使用场景如下:

def add(a, b)

这里 a 和 b 的类型在编译时都未知,有可能随着运行时发生变化。由于这个原因,JVM 首次执行 invokedynamic 调用时,它会查询一个 bootstrap 方法,该方法实现了依赖语言的逻辑,可以决定选择哪一个方法进行调用。bootstrap 方法返回一个链接调用点(linked call site)。很多情况下,如果 add 方法使用两个 int 类型的变量,紧接下来的调用也会使用两个 int 类型的值。所以,每次调用也没有必要都重新选择调用的方法。调用点自身就包含了一定的逻辑,可以判断在什么情况下需要进行重新链接。