1. 行为参数化

在 Java 8 之前,能作为参数进行传递的只能是一个确切类型的变量,比如:基本数据类型的变量,或者某个对象的引用变量。在面向对象程序设计中,对象主要包含两部分,属性和行为,在此,行为可以理解为就是对象中的方法。

Java 8 中的行为参数化,做到了可以把方法在调用链上进行传递。行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。

对集合进行排序是一个常见的编程任务。同一个集合列表,在不同的需求下可能排序规则不同,将排序行为参数化就可以应对这种变化的需求。

List<Integer> list = new ArrayList<>();
Collections.sort(list, (o1, o2) -> o1.compareTo(o2)); // 升序
Collections.sort(list, (o1, o2) -> o2.compareTo(o1)); // 倒序

2. Lambda表达式

Lambda 表达式可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

匿名
—— 因为它不像普通的方法那样有一个明确的名称。
函数
—— 因为 Lambda 函数不像方法那样属于某个特定的类。但和方法一样,Lambda 有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
传递
—— Lambda 表达式可以作为参数传递给方法或存储在变量中。
简洁
—— 无需像匿名类那样写很多模板代码。

2.1 语法规则

(parameters) -> expression

(parameters) -> { statements; }

2.2 函数式接口

一般在函数式接口上使用 Lambda 表达式,函数式接口就是只定义一个抽象方法的接口,比如:Runnable 接口的定义:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

2.3 函数描述符

函数式接口的抽象方法叫作函数描述符。在使用 Lambda 表达式的时候,Lambda 表达式的签名必须和函数式接口的函数描述符匹配才能正常使用。比如:

() -> void 代表了参数列表为空,且返回 void 的抽象方法。这正是 Runnable 函数式接口中的抽象方法。所以就可以在使用 Runnable 的地方使用这个 Lambda 表达式。

在 Java 8 的 API 中,函数式接口带有 @FunctionalInterface 的标注。这个标注用于表示该接口会设计成一个函数式接口。

2.4 付诸实践

从文件中一次读取一行内容,代码模板:

public static String processFile() throws IOException {    
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine();   // 功能代码
    }
}

如果一次读取两行内容,代码模板:

public static String processFile() throws IOException {    
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine() + br.readLine();   // 功能代码
    }
}

把 processFile 的行为参数化。然后需要一种方法把行为作为参数传递给 processFile ,以便它可以利用BufferedReader 执行不同的行为。

第1步:定义参数化行为

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

(BufferedReader br) -> br.readLine() + br.readLine() 就是要定义的行为。

第2步:定义函数式接口

@FunctionalInterface
public interface BufferedReaderProcessor {    
    String process(BufferedReader b) throws IOException;
}

函数式接口用来传递参数化的行为。

第3步:定义行为如何执行

public static String processFile(BufferedReaderProcessor p) throws IOException {    
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {        
        return p.process(br); // 功能代码
    }
}

第4步:传递 Lambda 表达式

String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

通过传递不同的 Lambda 重用 processFile 方法,并以不同的方式处理文件。

2.5 内置的函数式接口

上面展示了如何利用函数式接口来传递 Lambda ,但需要自己定义函数式接口才能使用。Java 8 中已经提供了一些常见的函数式接口,可以重用它们来传递不同的 Lambda,如果有现成的函数式接口,那么就不用我们自己定义并创建接口类了。

Predicate 接口
java.util.function.Predicate<T> 接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean 值。

@FunctionalInterface
public interface Predicate<T>{
    boolean test(T t);
}

Consumer 接口
java.util.function.Consumer<T> 定义了一个名叫accept 的抽象方法,它接受泛型T 的对象,没有返回(void)。

FunctionalInterface
public interface Consumer<T>{    
    void accept(T t);
}

Function 接口
java.util.function.Function<T, R> 接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。

@FunctionalInterface
public interface Function<T, R>{    
    R apply(T t);
}

Java 类型要么是引用类型(比如 Byte、Integer、Object、List),要么是原始类型(比如 int、double、byte、char)。但是泛型(比如 Consumer<T> 中的 T )只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在 Java 里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱 (boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱 (unboxing)。Java 还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。

但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。Java 8 为我们前面所说的函数式接口带来了专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如:IntPredicate。其他的还有 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction 等,这里就不一一举例子了。下面是 Java 8 提供的常用的函数式接口:

image.png

3. 流式数据处理

Java 8 中提供了流,用来实现对集合的声明式操作,就像数据库语言 SQL 那样,只需要指定具体的需求,比如:查找,分组等。可以把流看成遍历数据集的高级迭代器。此外,流还可以透明地
并行处理,无需写任何多线程代码等。流的使用一般包括三件事:

  1. 一个数据源(如集合)来执行一个查询;
  2. 一个中间操作链,形成一条流的流水线;
  3. 一个终端操作,执行流水线,并能生成结果。

3.1 筛选和切片

3.1.1 用谓词筛选

Streams 接口支持 filter 方法。该操作会接受一个谓词(一个返回 boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

Arrays.asList("a", "b", "c", "z", "d").stream().filter(s -> s.equals("a")).forEach(System.out::println);

3.1.2 筛选去重元素

流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的 hashCode 和 equals 方法实现)的流。

Arrays.asList(1, 2, 1, 3, 3, 2, 4).stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);

3.1.3 截短流

流支持 limit(n) 方法,该方法会返回一个不超过给定长度的流。

Arrays.asList("ab", "bc", "cd").stream().filter(s -> s.length() > 1).limit(2).forEach(System.out.println);

3.1.4 跳过元素

流还支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。

Arrays.asList("a", "b", "c", "d", "e", "f").stream().skip(10).forEach(System.out.println);

3.2 映射

一个非常常见的数据处理需求就是从某些对象中选择属性信息。就像在 SQL 里,可以从表中选择一列。Stream 也通过 map 和 flatMap 方法提供了类似的工具。

3.2.1 对元素应用函数

流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是 “创建一个新版本” 而不是去 “修改” )。

Arrays.asList("aaa", "bb", "c", "zzzz", "dd").stream().map(String::lengh).forEach(System.out.println);

3.2.2 将多个流融合

对字符串数组 ["ab", "bc", "cd", "zd", "dz"] 进行字符去重输出,输出结果为 a b c d z

使用 map 方法:

Stream<String[]> mapStream = Arrays.asList("ab", "bc", "cd", "zd", "dz").stream().map(s -> s.split(""));
mapStream.distinct().forEach(System.out::println);

输出结果:

[Ljava.lang.String;@7ba4f24f
[Ljava.lang.String;@3b9a45b3
[Ljava.lang.String;@7699a589
[Ljava.lang.String;@58372a00
[Ljava.lang.String;@4dd8dc3

未能达到预期结果。

使用 flatMap 方法:

Stream<String> flatMapStream = Arrays.asList("ab", "bc", "cd", "zd", "dz").stream().flatMap(s -> Stream.of(s.split("")));
flatMapStream.distinct().forEach(System.out::println);

输出结果:

a
b
c
d
z

达到预期输出结果。
总结,flatmap 方法可以把一个流中的每个值都换成一个流,然后把所有的流连接起来成为一个流。通过上面 map 方法 和 flatMap 方法能够比较直观的看到这一点。

3.3 查找和匹配

表达数据集中的某些元素是否匹配一个给定的属性。Stream API 通过 allMatch、anyMatch、 noneMatch、findFirst 和 findAny 方法提供了这样的实现。

3.3.1 检查谓词是否至少匹配一个元素

anyMatch 方法可以表达 “流中是否有一个元素能匹配给定的谓词” 。

boolean b = Arrays.asList("a", "b", "c", "d", "z").stream().anyMatch(e -> e.equals("b"));

b 的结果是 true。

3.3.3 检查谓词是否匹配所有元素

allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词。

boolean b = Arrays.asList("a", "b", "c", "d", "z").stream().allMatch(e -> e.equals("b"));

b 的结果是 false。

和 allMatch 相对的是 noneMatch 。它可以确保流中没有任何元素与给定的谓词匹配。

boolean b = Arrays.asList("a", "b", "c", "d", "z").stream().noneMatch(e -> e.equals("f"));

b 的结果是 true。

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

3.3.3 查找任意一个元素

findAny 方法将返回当前流中的任意元素。

Optional<String> any = Arrays.asList("a", "b", "c", "d", "z").stream().findAny();

3.3.4 查找第一个元素

有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List
或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个 findFirst
方法,它的工作方式类似于 findany。

Optional<String> first = Arrays.asList("a", "b", "c", "d", "z").stream().findFirst();
为什么会同时有 findFirst 和 findAny 呢?答案是并行。找到第一个元素在并行上限制更多,如果不关心返回的元素是哪个,请使用 findAny,因为它在使用并行流时限制较少。

3.4 归约

把一个流中的元素组合起来,使用 reduce 操作来表达更复杂的需求。此操作需要将流中所有元素反复结合起来,得到一个值,比如一个 Integer。这样的操作可以被归类为归约操作(将流归约成一个值)。

3.4.1 元素求和

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

int sum = numbers.stream().reduce(0, Integer::sum);

3.4.2 最大值和最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

3.4.3 map 和 reduce 组合

int count = menu.stream().map(d -> 1).reduce(0 , (a, b) -> a + b);

map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名,因为它很容易并行化。

3.5 构建流

3.5.1 由集合构建流

Stream<String> stream = Arrays.asList("a", "b", "c", "d", "z").stream();

3.5.2 由值创建流

使用静态方法 Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");

3.5.3 由数组创建流

int[] numbers = {2, 3, 5, 7, 11, 13};
IntStream stream = Arrays.stream(numbers);

3.5.4 由文件生成流

计算 data.txt 文件中的单词数量。

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) {
}
System.out.println(uniqueWords);

4. 并行流

Stream 接口可以通过调用 parallelStream 方法来把集合转换为并行流,或者对顺序流使用 parallel 方法来把其转化为并行流。并行流是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让它们都忙起来。

对顺序流使用 parallel 方法:

Stream.of("AAA", "BBB", "CCC").parallel().forEach(s -> System.out.println("Output:" + s));

对收集源使用 parallelStream 方法:

Arrays.asList("AAA", "BBB", "CCC").parallelStream().forEach(s -> System.out.println("Output:" + s));

5. CompletableFuture:组合式异步编程

5.1 异步执行任务

runAsync 方法接收一个消费者 Consumer 作为参数,Consumer 会交由 ForkJoinPool 池中的某个执行线程(Executor)运行。

CompletableFuture.runAsync(this::sendMessage);

supplyAsync 方法接受一个 Supplier 作为参数,返回一个 CompletableFuture 对象。调用 CompletableFuture 对象的 get 方法可以获取计算结果的值,Supplier 会交由 ForkJoinPool 池中的某个执行线程(Executor)运行。

CompletableFuture<Integer> result = CompletableFuture.supplyAsync(this::calculate);
Integer value = result.get();

5.2 增加回调

CompletableFuture.supplyAsync(this::calculate)
                      .thenAccept(this::sendMessage);

使用 thenAccept 方法实现 calculate 方法计算结果的回调,接收一个 Consumer 函数式接口。

5.3 链式回调

如果需要将 calculate 的计算结果从一个回调传递到另一个回调,thenAccept 方法做不到,因为它不会返回值。这个时候可以是用 thenApply 方法。

CompletableFuture.supplyAsync(this::calculate)
                     .thenApply(this::format)
                     .thenAccept(this::sendMessage);

5.4 thenCompose 方法

假设 calculate 方法返回 CompletionStage,如果继续使用 thenApply 最终会得到嵌套 CompletionStage。

CompletableFuture.supplyAsync(this::calculate)  
                 .thenApply(this::format);

// Returns type CompletionStage<CompletionStage<String>> 

上面嵌套的 CompletionStage 是我们不希望的,这个时候可以使用 thenCompose 方法。

CompletableFuture.supplyAsync(this::calculate)  
                 .thenCompose(this::format);

// Returns type CompletionStage<String>

5.5 独立线程执行回调

async 后缀的回调方法可以将回调提交给 ForkJoinPool.commonPool() 来使用独立的线程执行。

假设我们想一次向同一个接收者发送两条消息。

CompletableFuture<String> receiver  
            = CompletableFuture.supplyAsync(this::findReceiver);

receiver.thenApply(this::sendMsg);  
receiver.thenApply(this::sendOtherMsg);

在上面的示例中,sendMsg 和 sendOtherMsg 会使用一个线程执行。sendOtherMsg 必须等待 sendMsg 执行完成后才能执行。

使用 async 后缀的回调方法,sendMsg 和 sendOtherMsg 都作为单独的任务提交给 ForkJoinPool.commonPool()。各自在独立的线程中实现异步回调执行。

CompletableFuture<String> receiver  
            = CompletableFuture.supplyAsync(this::findReceiver);

receiver.thenApplyAsync(this::sendMsg);  
receiver.thenApplyAsync(this::sendOtherMsg);  

5.6 异常处理

exceptionally 方法可以实现如果前面的计算因异常而失败,可以对失败进行降级处理,返回失败替代结果。这样,后续回调可以继续以替代结果作为输入。

CompletableFuture.supplyAsync(this::failingMsg)  
                 .exceptionally(ex -> new Result(Status.FAILED))
                 .thenAccept(this::notify);

5.7 回调取决于多个计算

有时,能够创建一个依赖于两次计算结果的回调会非常有帮助。这时可以使用 thenCombine。thenCombine 允许 BiFunction 根据两个 CompletionStages 的结果注册一个回调。

public static String findUser() {
    return "小明";
}

public static String createMessage() {
    return "你好!";
}

public static String concat(String user, String content) {
    return user + "," + content;
}

public static void main(String[] args) throws Exception {
    CompletableFuture<String> user = CompletableFuture.supplyAsync(CompletableFutureTest::findUser);
    CompletableFuture<String> message = CompletableFuture.supplyAsync(CompletableFutureTest::createMessage);
    CompletableFuture<String> result = user.thenCombine(message, CompletableFutureTest::concat);
    System.out.println(result.get());
}

输出结果是:小明,您好!

6. 默认方法

Java 程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题。

Java 8 中的接口支持在声明方法的同时提供实现,通过两种方式可以完成这种操作。其一,Java 8 允许在接口内声明静态方法。其二,Java 8 引入了一个新功能,叫默认方法,通过默认方法你可以指定接口方法的默认实现。换句话说,接口能提供方法的具体实现。因此,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。比如:Collection 接口中的 stream 方法:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

默认方法由 default 修饰符修饰,并像类中声明的其他方法一样包含方法体。

参考

《Java 8 实战》


阿白
6 声望0 粉丝