Skip to content

Files

Latest commit

8814ef5 · Oct 11, 2021

History

History
1036 lines (787 loc) · 63.8 KB

File metadata and controls

1036 lines (787 loc) · 63.8 KB

十二、面向对象、函数式编程和 Lambda 表达式

在本章中,我们将讨论函数式编程以及 Java9 如何实现许多函数式编程概念。我们将用许多例子来说明如何将函数式编程与面向对象编程相结合。我们将:

  • 作为一等公民了解职能和方法
  • 使用函数接口和 lambda 表达式
  • 创建阵列筛选的功能版本
  • 创建具有泛型和接口的数据存储库
  • 具有复杂条件的筛选器集合
  • 使用映射操作转换值
  • 将映射操作与 reduce 相结合
  • 使用 map 和 reduce 链接许多操作
  • 与不同的收藏家合作

作为一等公民理解功能和方法

自从第一次发布以来,Java 一直是一种面向对象的编程语言。从 Java8 开始,Java 增加了对函数式编程范式的支持,并在 Java9 中继续这样做。函数式编程支持不可变的数据,因此,函数式编程避免状态更改。

用函数式编程风格编写的代码尽可能具有声明性,它关注于它做什么,而不是它必须如何做。

在大多数支持函数式编程范式的编程语言中,函数是一级公民,也就是说,我们可以将函数用作其他函数或方法的参数。Java8 引入了许多更改,以减少样板代码,使方法很容易成为 Java 中的一流公民,并使编写使用函数式编程方法的代码变得容易。我们可以通过一个简单的示例(例如筛选列表)轻松理解这个概念。但是,考虑到我们将以方法作为一级公民编写命令式代码,然后,我们将为该代码创建一个新版本,该版本使用 Java 9 中的完整函数方法,通过过滤操作。我们将创建这个示例的多个版本,因为它将允许我们理解函数式编程在 Java9 中是如何可能的。

首先,我们将编写一些代码,因为我们仍然不知道 Java9 中包含的将方法转换为一流公民的功能。然后,我们将在许多示例中使用这些特性。

以下几行声明了指定方法需求的Testable接口,该方法需求接收int类型的number参数并返回boolean结果。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_01.java文件中。

public interface Testable {
    boolean test(int number);
}

以下几行声明了实现先前声明的Testable接口的TestDivisibleBy5具体类。该类使用返回一个指示接收到的数字是否可被5整除的boolean值的代码实现test方法。如果数字与5之间的模、模或余数运算符(%的结果等于0,则表示该数字可被5整除。样本的代码文件包含在example12_01.java文件的java_9_oop_chapter_12_01文件夹中。

public class TestDivisibleBy5 implements Testable {
    @Override
    public boolean test(int number) {
        return ((number % 5) == 0);
    }
}

下面的行声明了实现先前声明的Testable接口的TestGreaterThan10具体类。该类实现了test方法,其代码返回一个boolean值,指示接收到的数字是否大于10。样本的代码文件包含在example12_01.java文件的java_9_oop_chapter_12_01文件夹中。

public class TestGreaterThan10 implements Testable {
    @Override
    public boolean test(int number) {
        return (number > 10);
    }
}

以下几行声明了在numbers参数中接收List<Integer>并在tester参数中接收Testable实例的filterNumbersWithTestable方法。该方法使用一个外部的for循环,即命令式代码为数字List<Integer>中的每个Integer元素调用tester.test方法。如果test方法返回true,则代码将Integer元素添加到filteredNumbersList<Integer>中,具体地说是一个ArrayList<Integer>。最后,该方法返回满足测试的所有Integer对象的filteredNumbersList<Integer>结果。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_01.java文件中。

public List<Integer> filterNumbersWithTestable(final List<Integer> numbers,
    Testable tester) {
    List<Integer> filteredNumbers = new ArrayList<>();
    for (Integer number : numbers) {
        if (tester.test(number)) {
            filteredNumbers.add(number);
        }
    }
    return filteredNumbers; 
}

filterNumbersWithTestable方法处理两个List<Integer>对象,即Integer对象中的两个List。我们谈论的是Integer而不是int原始类型。Integerint原语类型的包装类。但是,我们在Testable接口中声明并在实现该接口的两个类中实现的test方法接收到一个int类型的参数,而不是Integer

Java 自动将原语值转换为相应包装器类的对象。每当我们将一个对象作为参数传递给一个需要基元类型值的方法时,Java 编译器就会在一个称为取消装箱的操作中将该对象转换为相应的基元类型。在下一行中,Java 编译器将Integer对象转换或取消绑定为int类型的值。

if (tester.test(number)) {

编译器将执行相当于以下行的代码,该行调用将Integer解绑到intintValue()方法:

if (tester.test(number.intValue())) {

我们不会编写一个for循环来填充Integer对象的List。相反,我们将使用专门用于Stream<T>IntStream类来描述int原语流。这些类是在java.util.stream包中定义的,因此,我们必须添加import语句才能在 JShell 的代码中使用它。下一行调用以120为参数的IntStream.rangeClosed方法,生成一个IntStream,其中的int值从120(包括在内)。对boxed方法的链式调用将生成的IntStream转换为Stream<Integer>,即从原始int值装箱的Integer对象流。以Collectors.toList()为参数对collect方法的链式调用将Integer对象流收集到List<Integer>中,具体地说,收集到ArrayList<Integer>中。Collectors类也在java.util.stream包中定义。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_01.java文件中。

import java.util.stream.Collectors;
import java.util.stream.IntStream;

List<Integer> range1to20 = 
    IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

提示

装箱和取消装箱会增加开销,并会影响性能和内存。在某些情况下,我们可能需要重写代码,以避免在希望获得最佳性能时不必要的装箱和拆箱。

理解collect操作将开始处理管道以返回所需结果,即从中间流生成的列表,这一点非常重要。在调用collect方法之前,不会执行中间操作。下面的屏幕截图显示了在 JShell 中执行前几行的结果。我们可以看到,range1to20是一个Integer的列表,其中包含了装箱到Integer对象中的从 1 到 20(含)的数字。

Understanding functions and methods as first-class citizens

下面的行创建了名为testDivisibleBy5TestDivisibleBy5类的实例。然后,代码调用filterNumbersWithTestable方法,将List<Integer> range1to20作为numbers参数,将TestDivisibleBy5实例命名为testDivisibleBy5作为tester参数。代码运行后,List<Integer> divisibleBy5Numbers将具有以下值:[5, 10, 15, 20]。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_01.java文件中。

TestDivisibleBy5 testDivisibleBy5 = new TestDivisibleBy5();
List<Integer> divisibleBy5Numbers = 
filterNumbersWithTestable(range1to20, testDivisibleBy5);
System.out.println(divisibleBy5Numbers);

以下几行创建了名为testGreaterThan10TestGreaterThan10类的实例。然后,代码以range1to20testGreaterThan10作为参数调用filterNumbersWithTestable方法。代码运行后,List<Integer> greaterThan10Numbers将具有以下值:[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_01.java文件中。

TestGreaterThan10 testGreaterThan10 = new TestGreaterThan10();
List<Integer> greaterThan10Numbers = 
    filterNumbersWithTestable(range1to20, testGreaterThan10);
System.out.println(greaterThan10Numbers);

以下屏幕截图显示了在 JShell 中执行前几行的结果:

Understanding functions and methods as first-class citizens

使用功能接口和 lambda 表达式

我们必须声明一个接口和两个类,使一个方法能够接收Testable实例并执行每个类实现的test方法。幸运的是,Java 8 引入了功能接口,Java 9 使我们可以方便地在代码需要功能接口时提供兼容的lambda 表达式。简而言之,我们可以编写更少的代码来实现相同的目标。

功能接口是满足以下条件的接口:它具有单个抽象方法或单个方法需求。我们可以使用 lambda 表达式、方法引用或构造函数引用创建函数接口的实例。我们将使用不同的示例来理解 lambda 表达式、方法引用和构造函数引用,我们将看到它们的实际应用。

IntPredicate函数接口表示一个函数,该函数有一个int类型的参数,返回一个boolean结果。布尔值函数称为谓词。这个功能接口是在java.util.function中定义的,因此我们在使用它之前必须包含import语句。

以下几行声明了在numbers参数中接收List<Integer>和在predicate参数中接收IntPredicate实例的filterNumbersWithPredicate方法。此方法的代码与为filterNumbersWithTestable方法声明的代码相同,唯一的区别是,新方法接收的不是名为testerTestable类型的参数,而是名为predicateIntPredicate类型的参数。代码还使用从列表中检索到的每个数字作为参数来调用test方法进行计算。IntPredicate函数接口定义了一个名为test的抽象方法,该方法接收int并返回boolean结果。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_02.java文件中。

import java.util.function.IntPredicate;

public List<Integer> filterNumbersWithPredicate(final List<Integer> numbers,
    IntPredicate predicate) {
    List<Integer> filteredNumbers = new ArrayList<>();
    for (Integer number : numbers) {
        if (predicate.test(number)) {
            filteredNumbers.add(number);
        }
    }
    return filteredNumbers; 
}

下一行声明了一个名为divisibleBy5且类型为IntPredicate的变量,并为其分配了一个 lambda 表达式。具体来说,代码分配一个 lambda 表达式,该表达式接收名为nint参数,并返回一个boolean值,该值指示n5之间的模、模或余数运算符(%是否等于0。样本的代码文件包含在example12_02.java文件的java_9_oop_chapter_12_01文件夹中。

IntPredicate divisibleBy5 = n -> n % 5 == 0;

lambda 表达式由以下三个组成部分组成:

  • n:参数列表。在这种情况下,只有一个参数,因此,我们不需要将参数列表括在括号内。如果我们有多个参数,有必要将列表括在括号内。我们不必为参数指定类型。
  • ->:箭头标记。
  • n % 5 == 0:身体。在本例中,主体是单个表达式,因此不需要将其括在大括号中({}。另外,不需要在表达式前面写return语句,因为它是一个表达式。

前面的代码相当于下面的代码。上一行代码是最短版本,下一行代码是最长版本:

IntPredicate divisibleBy5 = (n) ->{ return n % 5 == 0 };

假设使用前面代码的两个版本中的任何一个,我们正在执行以下任务:

  1. 创建一个实现IntPredicate接口的匿名类。
  2. 在匿名类中声明一个测试方法,该方法接收一个int参数并返回一个boolean,其主体在箭头标记(->之后指定。
  3. 创建此匿名类的实例。

当需要IntPredicate时,只要我们输入 lambda 表达式,所有这些事情都会在引擎盖下发生。当我们将 lambda 表达式用于其他函数接口时,会发生类似的事情,不同之处在于方法名称、参数和方法的返回类型可能不同。

Java 编译器从函数接口推断参数的类型和返回类型。事情仍然是强类型的,如果我们在类型上出错,编译器将生成相应的错误,代码将无法编译。

以下几行调用filterNumbersWithPredicate方法,其中List<Integer> range1to20作为numbers参数,IntPredicate实例名为divisibleBy5作为predicate参数。代码运行后,List<Integer> divisibleBy5Numbers2将具有以下值:[5, 10, 15, 20]。样本的代码文件包含在example12_02.java文件的java_9_oop_chapter_12_01文件夹中。

List<Integer> divisibleBy5Numbers2 = 
    filterNumbersWithPredicate(range1to20, divisibleBy5);
System.out.println(divisibleBy5Numbers2);

以下几行调用filterNumbersWithPredicate方法,其中List<Integer> range1to20作为numbers参数,lambda 表达式作为predicate参数。lambda 表达式接收名为nint参数,并返回一个boolean值,指示n是否大于10。代码运行后,List<Integer> greaterThan10Numbers2将具有以下值:[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_02.java文件中。

List<Integer> greaterThan10Numbers2 = 
    filterNumbersWithPredicate(range1to20, n -> n > 10);
System.out.println(greaterThan10Numbers2);

下面的屏幕截图显示了在 JShell 中执行前几行的结果。

Working with functional interfaces and lambda expressions

Function<T, R>功能接口表示一个函数,其中T是该函数输入的类型,R是该函数结果的类型。我们不能为T指定一个灵长类类型,例如int,因为它不是一个类,但我们可以使用装箱类型,即Integer。我们不能将boolean用于R,但我们可以使用盒式,即Boolean。如果我们想要一个与IntPredicate函数接口类似的行为,我们可以使用Function<Integer, Boolean>,也就是说,一个函数具有一个返回Boolean结果的Integer类型的参数。这个功能接口是在java.util.function中定义的,因此在使用它之前必须包含import语句。

以下几行声明了在numbers参数中接收List<Integer>并在predicate参数中接收Function<Integer, Boolean>实例的filterNumbersWithFunction方法。此方法的代码与为filterNumbersWithCondition方法声明的代码相同,区别在于新方法接收的不是名为predicateIntPredicate类型的参数,而是名为functionFunction<Integer, Boolean>类型的参数。代码使用从列表中检索到的每个数字作为参数来调用apply方法,而不是调用test方法。

Function<T, R>功能接口定义了一个名为 apply 的抽象方法,该方法接收T并返回R类型的结果。在这种情况下,apply 方法接收到一个Integer并返回一个Boolean,Java 编译器将自动取消绑定到boolean。样本的代码文件包含在example12_03.java文件的java_9_oop_chapter_12_01文件夹中。

import java.util.function.Function;

public List<Integer> filterNumbersWithFunction(final List<Integer> numbers,
 Function<Integer, Boolean> function) {
    List<Integer> filteredNumbers = new ArrayList<>();
    for (Integer number : numbers) {
 if (function.apply(number)) {
            filteredNumbers.add(number);
        }
    }
    return filteredNumbers; 
}

以下几行调用filterNumbersWithFunction方法,其中List<Integer> range1to20作为numbers参数,一个 lambda 表达式作为function参数。lambda 表达式接收名为nInteger参数,并返回一个Boolean值,该值指示n3之间的模、模或余数运算符(%是否等于0。Java 自动将表达式生成的boolean值装箱到Boolean对象中。代码运行后,List<Integer> divisibleBy3Numbers将具有以下值:[3, 6, 9, 12, 15, 18]。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_03.java文件中。

List<Integer> divisibleBy3Numbers = 
    filterNumbersWithFunction(range1to20, n -> n % 3 == 0);

Java 将运行相当于以下代码行的代码。intValue()函数返回n中接收到的Integer实例的int值,lambda 表达式返回新Boolean实例中表达式求值生成的boolean值。但是,请记住,装箱和拆箱是在发动机罩下进行的。

List<Integer> divisibleBy3Numbers = 
    filterNumbersWithFunction(range1to20, n -> new Boolean(n.intValue() % 3 == 0));

java.util.function中定义了 40 多个功能接口。我们刚刚处理了其中两个,它们能够处理相同的 lambda 表达式。我们可以用一整本书来详细分析所有的功能接口。我们将继续关注面向对象与函数式编程的混合。但是,在声明定制接口之前,必须检查java.util.function中定义的所有功能接口,这一点非常重要。

创建阵列过滤的功能版本

前面的代码声明了filterNumbersWithFunction方法代表了带有外部for循环的数组过滤的强制版本。我们可以使用Stream<T>对象可用的filter方法,在本例中是Stream<Integer>对象,并通过函数方法实现相同的目标。

接下来的几行使用函数方法生成一个List<Integer>,其中的数字包含在可被3整除的List<Integer> range1to20中。样本的代码文件包含在example12_04.java文件的java_9_oop_chapter_12_01文件夹中。

List<Integer> divisibleBy3Numbers2 = range1to20.stream().filter(n -> n % 3 == 0).collect(Collectors.toList());

如果我们希望前面的代码在 JShell 中运行,那么我们必须在一行中输入所有代码,这对于 Java 编译器成功编译代码是不必要的。这是 JShell、streams 和 lambda 表达式的一个特定问题。这使得代码有点难以理解。因此,接下来的几行将显示另一个版本的代码,它使用多行代码,不会在 JShell 中工作,但会使代码更容易理解。请注意,在下面的示例中,您必须在一行中输入代码。代码文件使用单行。样本的代码文件包含在example12_04.java文件的java_9_oop_chapter_12_01文件夹中。

range1to20.stream()
.filter(n -> n % 3 == 0)
.collect(Collectors.toList());

提示

stream方法从List<Integer>生成Stream<Integer>是特定类型元素的序列,允许我们通过顺序或并行执行来执行计算或聚合操作。事实上,我们可以链接许多流操作并组成一个流管道。这些计算有一个延迟执行,也就是说,直到有一个终端操作,比如请求将最终数据收集到特定类型的List中,它们才会被计算。

filter方法接收Predicate<Integer>作为参数,我们将其应用于Stream<Integer>filter方法返回与指定谓词匹配的输入流元素流。该方法返回一个流,其中包含Predicate<Integer>计算结果为true的所有元素。我们将前面解释的 lambda 表达式作为filter方法的参数传递。

collect方法接收filter方法返回的Stream<Integer>。我们将Collectors.toList()作为参数传递给collect方法,对Stream<Integer>的元素执行可变约简操作,并生成一个List<Integer>,即可变结果容器。代码运行后,List<Integer> divisibleBy3Numbers2将具有以下值:[3, 6, 9, 12, 15, 18]

现在,我们希望采用函数方法打印生成的List<Integer>中的每个数字。List<T>实现Iterable<T>接口,该接口允许我们调用forEach方法来执行指定为Iterable中每个元素的参数的操作,直到所有元素都已处理完毕或该操作引发异常为止。forEach方法的动作参数必须是Consumer<T>,因此,在我们的例子中,它必须是Consumer<Integer>,因为我们将为结果List<Integer>调用forEach方法。

Consumer<T>是一个函数接口,表示访问T类型的单个输入参数且不返回结果(void的操作。Consumer<T>函数接口定义了一个名为accept的抽象方法,该方法接收类型为T的参数,但不返回任何结果。以下几行将 lambda 表达式作为参数传递给forEach方法。lambda 表达式生成一个Consumer<Integer>,打印n中接收到的数字。样本的代码文件包含在example12_04.java文件的java_9_oop_chapter_12_01文件夹中。

divisibleBy3Numbers2.forEach(n -> System.out.println(n));

作为前一行的结果,我们将在 JShell 中看到以下数字:

3
6
9
12
15
18

生成Consumer<Integer>的 lambda 表达式以Integer作为参数调用System.out.println方法。我们可以使用方法引用而不是 lambda 表达式来调用现有方法。在这种情况下,我们可以用System.out::println替换前面显示的 lambda 表达式,即调用System.outprintln方法的方法引用。每当我们使用方法引用时,Java 运行时都会推断方法类型参数;在这种情况下,方法类型参数是一个Integer。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_04.java文件中。

divisibleBy3Numbers2.forEach(System.out::println);

该代码将产生与前面使用 lambda 表达式调用forEach相同的结果。以下屏幕截图显示了在 JShell 中执行前几行的结果:

Creating a functional version of array filtering

我们可以捕获 lambda 表达式中未定义的变量。当 lambda 从外部世界捕获变量时,我们也可以称之为闭包。例如,以下几行声明一个名为byNumberint变量,并将4赋值给该变量。然后,接下来的几行使用 stream、filter 和 collect 组合的新版本生成一个数字可被byNumber变量中指定的数字整除的List<Integer>。lambda 表达式包括byNumber,Java 从外部世界捕获该变量。样本的代码文件包含在example12_04.java文件的java_9_oop_chapter_12_01文件夹中。

int byNumber = 4;
List<Integer> divisibleBy4Numbers =
    range1to20.stream().filter(
        n -> n % byNumber == 0).collect(
        Collectors.toList());
divisibleBy4Numbers.forEach(System.out::println);

作为前一行的结果,我们将在 JShell 中看到以下数字:

4
8
12
16
20

如果我们使用与函数接口不匹配的 lambda 表达式,代码将无法编译,Java 编译器将生成相应的错误。对于示例,下一行尝试将返回int而不是Booleanboolean的 lambda 表达式赋给IntPredicate变量。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_05.java文件中。

// The following code will generate an error
IntPredicate errorPredicate = n -> 8;

JShell 将显示以下错误,提示我们int无法转换为boolean

|  Error:
|  incompatible types: bad return type in lambda expression
|      int cannot be converted to boolean
|  IntPredicate errorPredicate = n -> 8;
|                                     ^

创建具有泛型和接口的数据存储库

现在我们想创建一个为我们提供实体的存储库,以便我们可以应用 Java 9 中包含的功能编程特性来从这些实体中检索和处理数据。首先,我们将创建一个Identifiable接口,定义可识别实体的需求。我们希望实现该接口的任何类都能提供一个getId方法,该方法返回一个int,其中包含实体的唯一标识符的值。样本的代码文件包含在example12_06.java文件的java_9_oop_chapter_12_01文件夹中。

public interface Identifiable {
    int getId();
}

接下来的几行创建一个泛型接口,该接口指定E必须在泛型类型约束中实现最近创建的泛型接口。该类声明了一个返回List<E>getAll方法。实现接口的每个类都必须为此方法提供自己的实现。样本的代码文件包含在example12_06.java文件的java_9_oop_chapter_12_01文件夹中。

public interface Repository<E extends Identifiable> {
    List<E> getAll();
}

接下来的几行创建Entity抽象类,它是所有实体的基类。类实现了Identifiable接口,定义了int类型的不可变id保护字段。构造函数接收id不可变字段的所需值,并用接收到的值初始化字段。抽象类实现了返回id不可变字段值的getId方法。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_06.java文件中。

public abstract class Entity implements Identifiable {
    protected final int id;

    public Entity(int id) {
        this.id = id;
    }

    @Override
    public final int getId() {
        return id;
    }
}

接下来的几行创建了MobileGame类,特别是先前创建的Entity抽象类的子类。样本的代码文件包含在example12_06.java文件的java_9_oop_chapter_12_01文件夹中。

public class MobileGame extends Entity {
    protected final String separator = "; ";
    public final String name;
    public int highestScore;
    public int lowestScore;
    public int playersCount;

    public MobileGame(int id, 
        String name, 
        int highestScore, 
        int lowestScore, 
        int playersCount) {
        super(id);
        this.name = name;
        this.highestScore = highestScore;
        this.lowestScore = lowestScore;
        this.playersCount = playersCount;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Id: ");
        sb.append(getId());
        sb.append(separator);
        sb.append("Name: ");
        sb.append(name);
        sb.append(separator);
        sb.append("Highest score: ");
        sb.append(highestScore);
        sb.append(separator);
        sb.append("Lowest score: ");
        sb.append(lowestScore);
        sb.append(separator);
        sb.append("Players count: ");
        sb.append(playersCount);

        return sb.toString();
    }
}

类声明了许多公共字段,这些字段的值由构造函数初始化:namehighestScorelowestScoreplayersCount。该字段是不可变的,但其他三个字段是可变的。我们不使用 getter 或 setter 来简化事情。但是,必须考虑到,一些允许我们使用实体的框架要求我们在字段不是只读的情况下对所有字段使用 getter 和 setter。

此外,该类重写从java.lang.Object类继承的toString方法,该方法必须为实体返回String表示。此方法中声明的代码使用java.lang.StringBuilder类(sb类)的实例高效地追加多个字符串,最后返回调用sb.toString方法返回生成的String的结果。此方法使用受保护的分隔符不可变字符串,该字符串确定我们在字段之间使用的分隔符。每当我们以MobileGame的实例作为参数调用System.out.println时,println方法将调用重写的toString方法来打印该实例的String表示。

提示

我们也可以使用String串联(+String.format来编写toString方法的代码,因为我们将只处理MobileGame类的 15 个实例。然而,每当我们需要连接多个字符串以产生结果时,使用StringBuilder是一种很好的实践,我们希望确保在执行代码时具有最佳性能。在我们的简单示例中,任何实现都不会有任何性能问题。

以下几行创建了实现Repository<MobileGame>接口的MemoryMobileGameRepository具体类。请注意,我们没有说Repository<E>,而是表示Repository<MobileGame>,因为我们已经知道我们将在类中实现的E类型参数的值。我们不是在创造一个MemoryMobileGameRepository<E extends Identifiable>。相反,我们正在创建一个非泛型的具体类,该类实现一个泛型接口,并将参数类型E的值设置为MobileGame。样本的代码文件包含在example12_06.java文件的java_9_oop_chapter_12_01文件夹中。

import java.util.stream.Collectors;

public class MemoryMobileGameRepository implements Repository<MobileGame> {
    @Override
    public List<MobileGame> getAll() {
        List<MobileGame> mobileGames = new ArrayList<>();
        mobileGames.add(
            new MobileGame(1, "Uncharted 4000", 5000, 10, 3800));
        mobileGames.add(
            new MobileGame(2, "Supergirl 2017", 8500, 5, 75000));
        mobileGames.add(
            new MobileGame(3, "Super Luigi Run", 32000, 300, 90000));
        mobileGames.add(
            new MobileGame(4, "Mario vs Kong III", 152000, 1500, 750000));
        mobileGames.add(
            new MobileGame(5, "Minecraft Reloaded", 6708960, 8000, 3500000));
        mobileGames.add(
            new MobileGame(6, "Pikachu vs Beedrill: The revenge", 780000, 400, 1000000));
        mobileGames.add(
            new MobileGame(7, "Jerry vs Tom vs Spike", 78000, 670, 20000));
        mobileGames.add(
            new MobileGame(8, "NBA 2017", 1500607, 20, 7000005));
        mobileGames.add(
            new MobileGame(9, "NFL 2017", 3205978, 0, 4600700));
        mobileGames.add(
            new MobileGame(10, "Nascar Remix", 785000, 0, 2600000));
        mobileGames.add(
            new MobileGame(11, "Little BIG Universe", 95000, 3, 546000));
        mobileGames.add(
            new MobileGame(12, "Plants vs Zombies Garden Warfare 3", 879059, 0, 789000));
        mobileGames.add(
            new MobileGame(13, "Final Fantasy XVII", 852325, 0, 375029));
        mobileGames.add(
            new MobileGame(14, "Watch Dogs 3", 27000, 2, 78004));
        mobileGames.add(
            new MobileGame(15, "Remember Me", 672345, 5, 252003));

        return mobileGames;
    }
}

类实现Repository<E>接口所需的getAll方法。在这种情况下,该方法返回MobileGameList<MobileGame>List,具体地说是一个ArrayList<MobileGame>。该方法创建 15 个MobileGame实例,并将它们附加到该方法返回的MobileGameArrayList中。

以下几行创建MemoryMobileGameRepository类的实例,并为getAll方法返回的List<MobileGame>调用forEach方法。forEach方法调用列表中每个元素的主体,就像在for循环中一样。指定为forEach方法参数的闭包调用System.out.println方法,并将MobileGame实例作为参数。这样,Java 使用MobileGame类中重写的toString方法为每个MobileGame实例生成String表示。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_06.java文件中。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository()
repository.getAll().forEach(mobileGame -> System.out.println(mobileGame));

以下几行显示了执行上一个代码后生成的输出,该代码为每个MobileGame实例打印toString()方法返回的String

Id: 1; Name: Uncharted 4000; Highest score: 5000; Lowest score: 10; Players count: 3800
Id: 2; Name: Supergirl 2017; Highest score: 8500; Lowest score: 5; Players count: 75000
Id: 3; Name: Super Luigi Run; Highest score: 32000; Lowest score: 300; Players count: 90000
Id: 4; Name: Mario vs Kong III; Highest score: 152000; Lowest score: 1500; Players count: 750000
Id: 5; Name: Minecraft Reloaded; Highest score: 6708960; Lowest score: 8000; Players count: 3500000
Id: 6; Name: Pikachu vs Beedrill: The revenge; Highest score: 780000; Lowest score: 400; Players count: 1000000
Id: 7; Name: Jerry vs Tom vs Spike; Highest score: 78000; Lowest score: 670; Players count: 20000
Id: 8; Name: NBA 2017; Highest score: 1500607; Lowest score: 20; Players count: 7000005
Id: 9; Name: NFL 2017; Highest score: 3205978; Lowest score: 0; Players count: 4600700
Id: 10; Name: Nascar Remix; Highest score: 785000; Lowest score: 0; Players count: 2600000
Id: 11; Name: Little BIG Universe; Highest score: 95000; Lowest score: 3; Players count: 546000
Id: 12; Name: Plants vs Zombies Garden Warfare 3; Highest score: 879059; Lowest score: 0; Players count: 789000
Id: 13; Name: Final Fantasy XVII; Highest score: 852325; Lowest score: 0; Players count: 375029
Id: 14; Name: Watch Dogs 3; Highest score: 27000; Lowest score: 2; Players count: 78004
Id: 15; Name: Remember Me; Highest score: 672345; Lowest score: 5; Players count: 252003

下一行产生相同的结果。在这种情况下,代码使用先前学习的引用方法调用System.out.println方法,将getAll方法返回的List<MobileGame>中的每个MobileGame实例作为参数。请注意,该行比上一个代码段的最后一行短,并且该代码生成相同的结果。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_06.java文件中。

repository.getAll().forEach(System.out::println);

过滤条件复杂的集合

我们可以使用新的存储库限制从复杂数据中检索的结果。我们可以将对getAll方法的调用与 stream、filter 和 collect 结合起来,生成Stream<MobileGame>,应用带有 lambda 表达式的过滤器作为参数,调用带有Collectors.toList()参数的collect方法,从过滤后的Stream<MobileGame>生成过滤后的List<MobileGame>filter方法接收Predicate<MobileGame>作为参数,我们使用 lambda 表达式生成该参数,并将过滤器应用于Stream<MobileGame>filter方法返回与指定谓词匹配的输入流元素流。该方法返回一个流,其中包含Predicate<MobileGame>计算结果为true的所有元素。

接下来的几行显示了使用多行代码的代码段,这些代码段在 JShell 中不起作用,但会使代码更易于阅读和理解。如果我们想让代码在 JShell 中运行,我们必须在一行中输入所有代码,这对于 Java 编译器成功编译代码是不必要的。这是 JShell、streams 和 lambda 表达式的一个特定问题。代码文件使用单行代码与 JShell 兼容。

以下几行声明了MemoryMobileGameRepository类的新getWithLowestScoreGreaterThan方法。请注意,为了避免重复,我们没有包含新类的所有代码。样本的代码文件包含在example12_07.java文件的java_9_oop_chapter_12_01文件夹中。

public List<MobileGame> getWithLowestScoreGreaterThan(int minimumLowestScore) {
    return getAll().stream()
        .filter(game -> game.lowestScore > minimumLowestScore)
        .collect(Collectors.toList());
}

以下几行使用名为repositoryMemoryMobileGameRepository实例调用前面添加的方法,然后链调用forEach打印lowestScore值大于1000的所有游戏:

MemoryMobileGameRepository repository = new MemoryMobileGameRepository()
repository.getWithLowestScoreGreaterThan(1000).forEach(System.out::println);

以下几行显示执行上一个代码后生成的输出:

Id: 4; Name: Mario vs Kong III; Highest score: 152000; Lowest score: 1500; Players count: 750000
Id: 5; Name: Minecraft Reloaded; Highest score: 6708960; Lowest score: 8000; Players count: 3500000

下一个代码显示了名为getWithLowestScoreGreaterThanV2getWithLowestScoreGreaterThan方法的另一个版本,它是等效的,并产生相同的结果。在本例中,生成Predicate<MobileGame>的 lambda 表达式使用括号并指定游戏参数的类型。如前一段代码所示,不需要这样做。但是,我们可以找到像下面几行那样编写的代码,因此,了解这种语法也可以正常工作是很重要的。样本的代码文件包含在example12_07.java文件的java_9_oop_chapter_12_01文件夹中。

public List<MobileGame> getWithLowestScoreGreaterThanV2(int minimumLowestScore) {
return getAll().stream()
 .filter((MobileGame game) -> game.lowestScore > minimumLowestScore) 
    .collect(Collectors.toList());
}

以下几行声明了MemoryMobileGameRepository类的新getStartingWith方法。作为参数传递给filter方法的 lambda 表达式返回调用startsWith方法获取游戏名称的结果,前缀作为参数接收。在本例中,lambda 表达式是捕获prefix参数并在 lambda 表达式体中使用它的闭包。样本的代码文件包含在example12_08.java文件的java_9_oop_chapter_12_01文件夹中。

public List<MobileGame> getStartingWith(String prefix) {
    return getAll().stream()
        .filter(game -> game.name.startsWith(prefix))
        .collect(Collectors.toList());
}

下面几行使用名为repositoryMemoryMobileGameRepository实例调用前面添加的方法,然后链接调用forEach打印所有名称以"Su"开头的游戏。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository()
repository.getStartingWith("Su").forEach(System.out::println);

以下几行显示执行上一个代码后生成的输出:

Id: 2; Name: Supergirl 2017; Highest score: 8500; Lowest score: 5; Players count: 75000
Id: 3; Name: Super Luigi Run; Highest score: 32000; Lowest score: 300; Players count: 90000

以下几行声明了MemoryMobileGameRepository类的新getByPlayersCountAndHighestScore方法。方法返回一个Optional<MobileGame>,即容器对象可能包含MobileGame实例,也可能是空的。如果有值,isPresent方法将返回true,我们可以通过调用get方法来检索MobileGame实例。在本例中,代码调用链接到filter方法调用的findFirst方法。findFirst方法返回一个Optional<T>,在这种情况下,返回一个Optional<MobileGame>,其中包含filter方法生成的Stream<MobileGame>中的第一个元素。请注意,我们在任何时候都不会对结果进行排序。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_09.java文件中。

public Optional<MobileGame> getByPlayersCountAndHighestScore(
    int playersCount, 
    int highestScore) {
    return getAll().stream()
        .filter(game -> (game.playersCount == playersCount) && (game.highestScore == highestScore))
        .findFirst();
}

下面几行使用名为repositoryMemoryMobileGameRepository实例来调用前面添加的方法。每次调用getByPlayersCountAndHighestScore方法后,代码都会调用isPresent方法,以确定Optional<MobileGame>是否有实例。如果该方法返回true,则代码调用get方法从Optional<MobileGame>中检索MobileGame实例。样本的代码文件包含在example12_09.java文件的java_9_oop_chapter_12_01文件夹中。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository()
Optional<MobileGame> optionalMobileGame1 = 
    repository.getByPlayersCountAndHighestScore(750000, 152000);
if (optionalMobileGame1.isPresent()) {
    MobileGame mobileGame1 = optionalMobileGame1.get();
    System.out.println(mobileGame1);
} else {
    System.out.println("No mobile game matches the specified criteria.");
}
Optional<MobileGame> optionalMobileGame2 = 
    repository.getByPlayersCountAndHighestScore(670000, 829340);
if (optionalMobileGame2.isPresent()) {
    MobileGame mobileGame2 = optionalMobileGame2.get();
    System.out.println(mobileGame2);
} else {
    System.out.println("No mobile game matches the specified criteria.");
}

以下几行显示了使用前面的代码生成的输出。在第一次通话中,有一款手机游戏符合搜索条件。在第二次调用中,没有与搜索条件匹配的MobileGame实例:

Id: 4; Name: Mario vs Kong III; Highest score: 152000; Lowest score: 1500; Players count: 750000
No mobile game matches the specified criteria.

以下屏幕截图显示了在 JShell 中执行前几行的结果:

Filtering collections with complex conditions

使用映射操作转换值

以下几行为我们之前编码的MemoryMobileGameRepository类声明了一个新的getGameNamesTransformedToUpperCase方法。新方法执行最简单的映射操作之一。对map方法的调用将Stream<MobileGame>转换为Stream<String>,作为参数传递给map方法的 lambda 表达式生成Function<MobileGame, String>,即接收MobileGame参数并返回String。对collect方法的调用根据map方法返回的Stream<String>生成List<String>

样本的代码文件包含在example12_10.java文件的java_9_oop_chapter_12_01文件夹中。

public List<String> getGameNamesTransformedToUpperCase() {
    return getAll().stream()
        .map(game -> game.name.toUpperCase())
        .collect(Collectors.toList());
}

getGameNamesTransformedToUpperCase方法返回一个List<String>map方法将Stream<MobileGame>中的每个MobileGame实例转换为String,并将name字段转换为大写。这样,map方法将Stream<MobileGame>转换为List<String>

以下几行使用名为repositoryMemoryMobileGameRepository实例调用之前添加的方法,并生成一个转换为大写字符串的游戏名称列表。样本的代码文件包含在example12_10.java文件的java_9_oop_chapter_12_01文件夹中。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository()
repository.getGameNamesTransformedToUpperCase().forEach(System.out::println);

以下几行显示执行上一个代码后生成的输出:

UNCHARTED 4000
SUPERGIRL 2017
SUPER LUIGI RUN
MARIO VS KONG III
MINECRAFT RELOADED
PIKACHU VS BEEDRILL: THE REVENGE
JERRY VS TOM VS SPIKE
NBA 2017
NFL 2017
NASCAR REMIX
LITTLE BIG UNIVERSE
PLANTS VS ZOMBIES GARDEN WARFARE 3
FINAL FANTASY XVII
WATCH DOGS 3
REMEMBER ME

下面的代码使用两个构造函数创建一个新的NamesForMobileGame类。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_11.java文件中。

public class NamesForMobileGame {
    public final String upperCaseName;
    public final String lowerCaseName;

    public NamesForMobileGame(String name) {
        this.upperCaseName = name.toUpperCase();
        this.lowerCaseName = name.toLowerCase();
    }

    public NamesForMobileGame(MobileGame game) {
        this(game.name);
    }
}

NamesForMobileGame类声明了String类型的两个不可变字段:upperCaseNamelowerCaseName。其中一个构造函数收到一个nameString并将其转换为大写保存在upperCaseName字段中,将其转换为小写保存在lowerCaseName字段中。另一个构造函数接收到一个MobileGame实例,并调用前面解释的构造函数,将接收到的MobileGame实例的name字段作为参数。

下面的代码向MemoryMobileGameRepository类添加了一个新的getNamesForMobileGames方法。新方法执行映射操作。对map方法的调用将Stream<MobileGame>转换为Stream<NamesForMobileGame>。作为参数传递给map方法的 lambda 表达式生成一个Function<MobileGame, NamesForMobileGame>,也就是说,它接收一个MobileGame参数,并通过调用接收一个name作为参数的构造函数返回一个NamesForMobileGame实例。对collect方法的调用根据map方法返回的Stream<NamesForMobileGame>生成List<NamesForMobileGame>。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_11.java文件中。

public List<NamesForMobileGame> getNamesForMobileGames() {
    return getAll().stream()
        .map(game -> new NamesForMobileGame(game.name))
        .collect(Collectors.toList());
}

下面几行使用名为repositoryMemoryMobileGameRepository实例来调用前面添加的方法。lambda 表达式作为参数传递给forEach方法,它声明了一个用大括号括起来的体,因为它需要很多行。这个主体使用java.lang.StringBuilder类(sb类)的一个实例来附加许多带有大写名称、分隔符和小写名称的字符串。样本的代码文件包含在example12_11.java文件的java_9_oop_chapter_12_01文件夹中。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository()
repository.getNamesForMobileGames().forEach(names -> {
    StringBuilder sb = new StringBuilder();
    sb.append(names.upperCaseName);
    sb.append(" - ");
    sb.append(names.lowerCaseName);
    System.out.println(sb.toString());
});

以下几行显示执行上一个代码后生成的输出:

UNCHARTED 4000 - uncharted 4000
SUPERGIRL 2017 - supergirl 2017
SUPER LUIGI RUN - super luigi run
MARIO VS KONG III - mario vs kong iii
MINECRAFT RELOADED - minecraft reloaded
PIKACHU VS BEEDRILL: THE REVENGE - pikachu vs beedrill: the revenge
JERRY VS TOM VS SPIKE - jerry vs tom vs spike
NBA 2017 - nba 2017
NFL 2017 - nfl 2017
NASCAR REMIX - nascar remix
LITTLE BIG UNIVERSE - little big universe
PLANTS VS ZOMBIES GARDEN WARFARE 3 - plants vs zombies garden warfare 3
FINAL FANTASY XVII - final fantasy xvii
WATCH DOGS 3 - watch dogs 3
REMEMBER ME - remember me

下一个代码显示了名为getNamesForMobileGamesV2getNamesForMobileGames方法的另一个版本,它是等效的,并产生相同的结果。在本例中,我们用构造函数引用方法NamesForMobileGame::new替换了生成Function<MobileGame, NamesForMobileGame>的 lambda 表达式。构造函数引用方法使用类名后跟::new进行指定,并将使用接收MobileGame实例作为参数的构造函数创建NamesForMobileGame的新实例。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_12.java文件中。

public List<NamesForMobileGame> getNamesForMobileGamesV2() {
    return getAll().stream()
        .map(NamesForMobileGame::new)
        .collect(Collectors.toList());
}

下面的代码使用新版本的方法,并且生成与第一个版本相同的结果。样本的代码文件包含在example12_12.java文件的java_9_oop_chapter_12_01文件夹中。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository();
repository.getNamesForMobileGamesV2().forEach(names -> {
    StringBuilder sb = new StringBuilder();
    sb.append(names.upperCaseName);
    sb.append(" - ");
    sb.append(names.lowerCaseName);
    System.out.println(sb.toString());
});

将 map 操作与 reduce 相结合

以下几行显示了一个for循环的命令式代码版本,计算手机游戏的所有lowestScore值之和。样本的代码文件包含在example12_13.java文件的java_9_oop_chapter_12_01文件夹中。

int lowestScoreSum = 0;
for (MobileGame mobileGame : repository.getAll()) {
    lowestScoreSum += mobileGame.lowestScore;
}
System.out.println(lowestScoreSum);

代码很容易理解。lowestScoreSum变量的起始值为0,并且for循环的每次迭代从repository.getAll()方法返回的List<MobileGame>中检索一个MobileGame实例,并用mobileGame.lowestScore 字段的值增加lowestScoreSum变量的值。

我们可以将 map 和 reduce 操作结合起来,创建之前命令式代码的功能版本,以计算手机游戏的所有lowestScore值之和。接下来的几行将呼叫map链接到reduce以实现此目标。看看下面的代码。样本的代码文件包含在example12_14.java文件的java_9_oop_chapter_12_01文件夹中。

int lowestScoreMapReduceSum = repository.getAll().stream().map(game -> game.lowestScore).reduce(0, (sum, lowestScore) -> sum + lowestScore);
System.out.println(lowestScoreMapReduceSum);

首先,代码使用对map的调用将Stream<MobileGame>转换为Stream<Integer>,并将lowestScore存储属性中指定的值装箱到Integer对象中。然后,代码调用reduce方法,该方法接收两个参数:的初始值为累积值0,以及将使用累积值重复调用的组合闭包。该方法返回对 combine 闭包重复调用的结果。

reduce方法的第二个参数中指定的闭包接收sumlowestScore,并返回两个值的总和。因此,闭包返回到目前为止累计的总和加上处理的lowestScore值。我们可以添加一个System.out.println语句来显示reduce方法的第二个参数中指定的闭包中sumlowestScore的值。以下几行显示了先前代码的新版本,该代码添加了带有System.out.println语句的行,这将使我们能够深入了解reduce操作的工作原理。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_15.java文件中。

int lowestScoreMapReduceSum2 = 
    repository.getAll().stream()
    .map(game -> game.lowestScore)
    .reduce(0, (sum, lowestScore) -> {
        StringBuilder sb = new StringBuilder();
        sb.append("sum value: ");
        sb.append(sum);
        sb.append(";lowestScore value: ");
        sb.append(lowestScore);
        System.out.println(sb.toString());

        return sum + lowestScore;
    });
System.out.println(lowestScoreMapReduceSum2);

以下几行显示了前几行的结果,我们可以看到sum参数的值是如何从reduce方法(0的第一个参数中指定的初始值开始的,并累计到目前为止完成的总和。最后,lowestScoreSum2变量保存所有lowestScore值的总和。我们可以看到,为sumlowestScore打印的最后一个值是109105。为 reduce 操作执行的最后一段代码计算109105并返回10915,这是保存在lowestScoreSum2变量中的结果。

sum value: 0; lowestScore value: 10
sum value: 10; lowestScore value: 5
sum value: 15; lowestScore value: 300
sum value: 315; lowestScore value: 1500
sum value: 1815; lowestScore value: 8000
sum value: 9815; lowestScore value: 400
sum value: 10215; lowestScore value: 670
sum value: 10885; lowestScore value: 20
sum value: 10905; lowestScore value: 0
sum value: 10905; lowestScore value: 0
sum value: 10905; lowestScore value: 3
sum value: 10908; lowestScore value: 0
sum value: 10908; lowestScore value: 0
sum value: 10908; lowestScore value: 2
sum value: 10910; lowestScore value: 5
lowestScoreMapReduceSum2 ==> 10915
10915

在前面的示例中,我们将 map 和 reduce 组合起来执行求和。我们可以利用 Java9 提供的简化方法,通过简化代码实现相同的目标。在下面的代码中,我们利用mapToInt生成IntStream;总和与int值一起工作,不必将Integer解组为int。样本的代码文件包含在example12_16.java文件的java_9_oop_chapter_12_01文件夹中。

int lowestScoreMapReduceSum3 =
    repository.getAll().stream()
    .mapToInt(game -> game.lowestScore).sum();
System.out.println(lowestScoreMapReduceSum3);

接下来的几行还使用不同的管道生成相同的结果,该管道的效率不如前面显示的管道。map方法必须将返回的int框入Integer并返回Stream<Integer>。然后,对collect方法的调用指定对Collectors.summingInt的调用作为参数。Collectors.summingInt需要int值来计算总和,因此,我们传递一个方法引用来为Stream<Integer>中的每个Integer调用intValue方法。以下行使用Collectors.summingInt采集器执行int值之和。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_17.java文件中。

int lowestScoreMapReduceSum4 = 
    repository.getAll().stream()
.map(game -> game.lowestScore)
.collect(Collectors.summingInt(Integer::intValue));
System.out.println(lowestScoreMapReduceSum4);

在这种情况下,我们知道Integer.MAX_VALUE将允许我们保持总和的准确结果。但是,在某些情况下,我们必须使用类型。下面的代码使用mapToLong方法使用long来累积值。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_18.java文件中。

long lowestScoreMapReduceSum5 =
    repository.getAll().stream()
    .mapToLong(game -> game.lowestScore).sum();
System.out.println(lowestScoreMapReduceSum6);

提示

Java9 提供了许多简化方法,也称为聚合操作。在编写自己的代码以执行诸如计数、平均值和求和等操作之前,请务必考虑这些问题。我们可以使用它们对流执行算术运算并得到数字结果。

使用 map 和 reduce 链接多个操作

我们可以连锁filtermapreduce操作。下面的代码向MemoryMobileGameRepository类添加了一个新的getHighestScoreSumForMinPlayersCount方法。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的文件example12_19.java中。

public long getHighestScoreSumForMinPlayersCount(int minPlayersCount) {
    return getAll().stream()
        .filter(game -> (game.playersCount >= minPlayersCount))
        .mapToLong(game -> game.highestScore)
        .reduce(0, (sum, highestScore) -> sum + highestScore);
}

新方法执行一个与一个mapToLong链接的filter操作,最后执行一个reduce操作。对filter的调用生成一个Stream<MobileGame>,其中MobileGame的实例的playersCount值等于或大于作为参数接收的minPlayersCount值。mapToLong方法返回一个LongStream,即描述long原语流的专用Stream<T>。对mapToLong的调用为每个过滤的MobileGame实例接收int类型的highestScore值,并将该值转换为long

reduce方法从处理管道接收LongStreamreduce运算的累积值的初始值被指定为第一个参数0,第二个参数是一个 lambda 表达式,其组合运算将被用累积值重复调用。方法将重复调用的结果返回给联合收割机操作。

reduce方法的第二个参数中指定的 lambda 表达式接收sumhighestScore,并返回两个值的总和。因此,lambda 表达式返回到目前为止在sum参数中接收到的累计总数加上已处理的highestScore值之和。

下一行使用先前创建的方法。样本的代码文件包含在example12_19.java文件的java_9_oop_chapter_12_01文件夹中。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository();
System.out.println(repository.getHighestScoreSumForMinPlayersCount(150000));

JShell 将显示以下值作为结果:

15631274

正如我们从前面的示例中了解到的,我们可以使用sum方法,而不是为reduce方法编写代码。下一个代码显示名为getHighestScoreSumForMinPlayersCountV2getHighestScoreSumForMinPlayersCount方法的另一个版本,它是等效的,并产生相同的结果。样本的代码文件包含在java_9_oop_chapter_12_01文件夹中的example12_20.java文件中。

public long getHighestScoreSumForMinPlayersCountV2(int minPlayersCount) {
    return getAll().stream()
        .filter(game -> (game.playersCount >= minPlayersCount))
        .mapToLong(game -> game.highestScore)
        .sum();
}

下面的代码使用该方法的新版本,并生成与第一个版本相同的结果。样本的代码文件包含在example12_20.java文件的java_9_oop_chapter_12_01文件夹中。

MemoryMobileGameRepository repository = new MemoryMobileGameRepository();
System.out.println(repository.getHighestScoreSumForMinPlayersCountV2(150000));

使用不同的收集器

我们可以遵循函数方法,通过流处理管道和 Java 9 提供的各种收集器(即,java.util.stream.Collectors类提供的各种静态方法)来解决各种算法。在下一个示例中,我们将为collect方法使用不同的参数。

以下几行将MobileGame实例的所有名称连接起来,生成一个String,名称之间用分隔符("; "分隔。样本的代码文件包含在example12_21.java文件的java_9_oop_chapter_12_01文件夹中。

repository.getAll().stream()
.map(game -> game.name.toUpperCase())
.collect(Collectors.joining("; "));

代码将Collectors.joining(";" )作为参数传递给collect方法。joining静态方法返回一个Collector,该方法将输入元素连接成一个String,由作为参数接收的分隔符分隔。下面显示了在 JShell 中执行前几行的结果。

UNCHARTED 4000; SUPERGIRL 2017; SUPER LUIGI RUN; MARIO VS KONG III; MINECRAFT RELOADED; PIKACHU VS BEEDRILL: THE REVENGE; JERRY VS TOM VS SPIKE; NBA 2017; NFL 2017; NASCAR REMIX; LITTLE BIG UNIVERSE; PLANTS VS ZOMBIES GARDEN WARFARE 3; FINAL FANTASY XVII; WATCH DOGS 3; REMEMBER ME

以下几行显示了前一个代码段的新版本,该代码段按名称升序排列结果。样本的代码文件包含在example12_22.java文件的java_9_oop_chapter_12_01文件夹中。

repository.getAll().stream().sorted(Comparator.comparing(game -> game.name)).map(game -> game.name.toUpperCase()).collect(Collectors.joining("; "));

代码将Comparator.comparing(game -> game.name)作为参数传递给sorted方法。comparing静态方法接收一个函数,该函数从MobileGame提取所需的排序键,并返回一个Comparator<MobileGame>,该函数使用指定的比较器比较该排序键。代码将 lambda 表达式作为参数传递给comparing静态方法,以指定名称作为MobileGame实例所需的排序键。排序方法接收一个Stream<MobileGame>并返回一个Stream<MobileGame>,其中MobileGame实例按照提供的Comparator<MobileGame>进行排序。下面显示了在 JShell 中执行前几行的结果:

FINAL FANTASY XVII; JERRY VS TOM VS SPIKE; LITTLE BIG UNIVERSE; MARIO VS KONG III; MINECRAFT RELOADED; NBA 2017; NFL 2017; NASCAR REMIX; PIKACHU VS BEEDRILL: THE REVENGE; PLANTS VS ZOMBIES GARDEN WARFARE 3; REMEMBER ME; SUPER LUIGI RUN; SUPERGIRL 2017; UNCHARTED 4000; WATCH DOGS 3

现在我们要检查玩家数量等于或高于指定阈值的游戏。我们要检查通过和失败的比赛。以下几行生成一个Map<Boolean, List<MobileGame>>,其键指定手机游戏是否通过,该值包括通过或失败的List<MobileGame>。然后,代码调用forEach方法来显示结果。样本的代码文件包含在example12_23.java文件的java_9_oop_chapter_12_01文件夹中。

Map<Boolean, List<MobileGame>> map1 = 
repository.getAll().stream()
.collect(Collectors.partitioningBy(g -> g.playersCount >= 100000));
map1.forEach((passed, mobileGames) -> {
    System.out.println(
        String.format("Mobile games that %s:",
            passed ? "passed" : "didn't pass"));
    mobileGames.forEach(System.out::println);
});

代码将Collectors.partitioningBy(g -> g.playersCount >= 100000)作为参数传递给collect方法。partitioningBy静态方法接收Predicate<MobileGame>信号。代码将 lambda 表达式作为参数传递给partitioningBy静态方法,以指定必须根据playersCount字段是否大于或等于100000对输入元素进行分区。返回的Collector<MobileGame>Stream<MobileGame>进行分区,组织成Map<Boolean, List<MobileGame>>,进行下游归约。

然后,代码使用 lambda 表达式作为参数调用forEach方法,该参数从passedmobileGames参数中的Map<Boolean, List<MobileGame>>接收键和值。下面显示了在 JShell 中执行前几行的结果:

Mobile games that didn't pass:
Id: 1; Name: Uncharted 4000; Highest score: 5000; Lowest score: 10; Players count: 3800
Id: 2; Name: Supergirl 2017; Highest score: 8500; Lowest score: 5; Players count: 75000
Id: 3; Name: Super Luigi Run; Highest score: 32000; Lowest score: 300; Players count: 90000
Id: 7; Name: Jerry vs Tom vs Spike; Highest score: 78000; Lowest score: 670; Players count: 20000
Id: 14; Name: Watch Dogs 3; Highest score: 27000; Lowest score: 2; Players count: 78004
Mobile games that passed:
Id: 4; Name: Mario vs Kong III; Highest score: 152000; Lowest score: 1500; Players count: 750000
Id: 5; Name: Minecraft Reloaded; Highest score: 6708960; Lowest score: 8000; Players count: 3500000
Id: 6; Name: Pikachu vs Beedrill: The revenge; Highest score: 780000; Lowest score: 400; Players count: 1000000
Id: 8; Name: NBA 2017; Highest score: 1500607; Lowest score: 20; Players count: 7000005
Id: 9; Name: NFL 2017; Highest score: 3205978; Lowest score: 0; Players count: 4600700
Id: 10; Name: Nascar Remix; Highest score: 785000; Lowest score: 0; Players count: 2600000
Id: 11; Name: Little BIG Universe; Highest score: 95000; Lowest score: 3; Players count: 546000
Id: 12; Name: Plants vs Zombies Garden Warfare 3; Highest score: 879059; Lowest score: 0; Players count: 789000
Id: 13; Name: Final Fantasy XVII; Highest score: 852325; Lowest score: 0; Players count: 375029
Id: 15; Name: Remember Me; Highest score: 672345; Lowest score: 5; Players count: 252003

以下几行显示了前一个代码段的新版本,该代码段按名称升序排列每个生成分区的结果。添加排序的行将高亮显示。样本的代码文件包含在example12_24.java文件的java_9_oop_chapter_12_01文件夹中。

Map<Boolean, List<MobileGame>> map1 =
repository.getAll().stream()
.sorted(Comparator.comparing(game -> game.name))
.collect(Collectors.partitioningBy(g -> g.playersCount >= 100000));
map1.forEach((passed, mobileGames) -> {
    System.out.println(
        String.format("Mobile games that %s:",
            passed ? "passed" : "didn't pass"));
    mobileGames.forEach(System.out::println);
});

下面显示了在 JShell 中执行前几行的结果:

Mobile games that didn't pass:
Id: 7; Name: Jerry vs Tom vs Spike; Highest score: 78000; Lowest score: 670; Players count: 20000
Id: 3; Name: Super Luigi Run; Highest score: 32000; Lowest score: 300; Players count: 90000
Id: 2; Name: Supergirl 2017; Highest score: 8500; Lowest score: 5; Players count: 75000
Id: 1; Name: Uncharted 4000; Highest score: 5000; Lowest score: 10; Players count: 3800
Id: 14; Name: Watch Dogs 3; Highest score: 27000; Lowest score: 2; Players count: 78004
Mobile games that passed:
Id: 13; Name: Final Fantasy XVII; Highest score: 852325; Lowest score: 0; Players count: 375029
Id: 11; Name: Little BIG Universe; Highest score: 95000; Lowest score: 3; Players count: 546000
Id: 4; Name: Mario vs Kong III; Highest score: 152000; Lowest score: 1500; Players count: 750000
Id: 5; Name: Minecraft Reloaded; Highest score: 6708960; Lowest score: 8000; Players count: 3500000
Id: 8; Name: NBA 2017; Highest score: 1500607; Lowest score: 20; Players count: 7000005
Id: 9; Name: NFL 2017; Highest score: 3205978; Lowest score: 0; Players count: 4600700
Id: 10; Name: Nascar Remix; Highest score: 785000; Lowest score: 0; Players count: 2600000
Id: 6; Name: Pikachu vs Beedrill: The revenge; Highest score: 780000; Lowest score: 400; Players count: 1000000
Id: 12; Name: Plants vs Zombies Garden Warfare 3; Highest score: 879059; Lowest score: 0; Players count: 789000
Id: 15; Name: Remember Me; Highest score: 672345; Lowest score: 5; Players count: 252003

测试你的知识

  1. 功能接口是满足以下条件的接口:
    1. 它在其默认方法之一中使用 lambda 表达式。
    2. 它有一个单一的抽象方法或单一的方法要求。
    3. 实现Lambda<T, U>接口。
  2. 您可以通过以下方式创建功能接口的实例:
    1. Lambda 表达式、方法引用或构造函数引用。
    2. 只有 lambda 表达式。方法引用和构造函数引用仅适用于Predicate<T>
    3. 方法引用和构造函数引用。Lambda 表达式仅适用于Predicate<T>
  3. IntPredicate功能接口代表一个具有以下功能的功能:
    1. int类型的一个参数,不返回任何结果(void
    2. 一个返回Integer结果的int类型的参数。
    3. 一个返回boolean结果的int类型的参数。
  4. 当我们将filter方法应用于Stream<T>时,该方法返回:
    1. AStream<T>
    2. AList<T>
    3. AMap<T, List<T>>
  5. 以下哪一个代码段等同于numbers.forEach(n -> System.out.println(n));
    1. numbers.forEach(n::System.out.println);
    2. numbers.forEach(System.out::println);
    3. numbers.forEach(n ->System.out.println);

总结

在本章中,我们使用了 Java9 中包含的许多函数式编程特性,并将它们与我们到目前为止讨论的所有面向对象编程结合起来。对于许多算法,我们分析了命令式代码和函数式编程方法之间的差异。

我们使用函数接口和 lambda 表达式。我们理解方法引用和构造函数引用。我们创建了一个包含泛型和接口的数据存储库,并使用它处理过滤器、映射操作、缩减、聚合函数、排序和分区。我们使用不同的流处理管道。

现在您已经了解了函数式编程,我们准备利用 Java9 中的模块化,这是我们将在下一章讨论的主题。