Java9-秘籍-三-

66 阅读1小时+

Java9 秘籍(三)

原文:Java 9 Recipes

协议:CC BY-NC-SA 4.0

六、Lambda 表达式

现有语言中的新特性能够对生态系统产生重大影响的方法非常少。Java 语言的 Lambda 表达式就是这样一个重要的新特性,它对生态系统的许多方面都产生了影响。简单来说, lambda 表达式是一种创建匿名函数的便捷方式。它们提供了一种使用一个表达式或一系列语句创建单个方法接口的简单方法。Lambda 表达式建立在函数接口之上,函数接口是包含单一抽象方法的接口。它们可以应用在许多不同的环境中,从简单的匿名函数到排序和过滤集合。而且,lambda 表达式可以赋值给变量,然后传递给其他对象。

在这一章中,你将学习如何创建 lambda 表达式,并且你将会看到它们如何应用于常见场景的许多例子。您还将学习如何为 lambda 表达式生成构建块,这样您就可以构建应用来方便它们的使用。本章将深入探讨 java.util.function 包,它包含一组 lambdas 可以实现的有用的函数接口。最后,您将看到如何将特定类型的 lambda 表达式简化为方法引用,以获得更简洁的方法。

读完这一章,你也将能够看到 lambda 表达式对 Java 语言的影响。它们通过允许开发人员更有效率来使语言现代化,并在许多领域开辟了新的可能性。Lambda 表达式翻开了 Java 的新篇章,将这种语言带入了一个新的领域,类似的其他语言已经有了类似的结构。这些语言为 Java 语言中的 lambda 表达式铺平了道路,毫无疑问,lambda 表达式将继续为许多优雅的解决方案铺平道路。

6-1.编写简单的 Lambda 表达式

问题

您希望封装一个打印出简单消息的功能。

解决办法

编写一个 lambda 表达式,该表达式接受包含要打印的消息的单个参数,并在 lambda 中实现打印功能。在下面的示例中,函数接口 HelloType 通过 lambda 表达式实现,并赋给变量 helloLambda。最后,lambda 被调用,打印消息。

public class HelloLambda {

    /**
     * Functional Interface
     */
    public interface HelloType {
        /**
         * Function that will be implemented within the lambda
         * @param text
         */
        void hello(String text);
    }

    public static void main(String[] args){
        // Create the lambda, passing a parameter named "text" to the
        // hello() method, returning the String.  The lambda is assigned
        // to the helloLambda variable.
        HelloType helloLambda =
                (String text) -> {System.out.println("Hello " + text);};

        // Invoke the method call
        helloLambda.hello("Lambda");
    }
}

结果:

Hello Lambda

它是如何工作的

lambda 表达式是一个匿名代码块,它封装了一个表达式或一系列语句,并返回一个结果。Lambda 表达式在其他一些语言中也被称为闭包。它们可以接受零个或多个参数,其中任何一个参数都可以通过指定或不指定类型来传递,因为类型可以从上下文中自动派生。

lambda 表达式的语法包括一个参数列表、一个称为“箭头标记”(-->)的语言新字符和一个主体。以下模型表示 lambda 表达式的结构:

(argument list) -> { body }

lambda 表达式的论点单可以包含零个或多个参数。如果没有参数,那么可以使用一组空括号。如果只有一个参数,则不需要括号。列表中的每个参数都可以包含一个可选的类型规范。如果省略了参数的类型,则该类型是从当前上下文中派生的。

在这个配方的解决方案中,花括号括住了一个块的主体,它包含不止一个表达式。如果主体由单个表达式组成,则不需要花括号。解决方案中的花括号本来可以去掉,但是为了便于阅读,还是包括了花括号。身体被简单地评估,然后被返回。如果 lambda 的主体是表达式而不是语句,则返回是隐式的。相反,如果主体包含一个以上的语句,则必须指定一个 return,它标志着控制权返回给调用者。

以下代码演示了一个不包含任何参数的 lambda 表达式:

StringReturn msg = () ->  "This is a test";

lambda 使用的 StringReturn 接口也称为函数接口。

/**
 * Functional interface returning a String
 */
 interface StringReturn {
    String returnMessage();
}

让我们来看看这个 lambda 表达式是如何工作的。在前面的清单中,从 lambda 表达式返回一个 StringReturn 类型的对象。空括号表示没有参数传递给表达式。返回是隐式的,字符串“这是一个测试”从 lambda 表达式返回给调用者。示例中的表达式被赋给一个由 msg 标识的变量。假设函数接口 StringReturn 包含一个标识为 returnMessage()的抽象方法,如代码所示。在这种情况下,可以调用 msg.returnMessage()方法来返回字符串。

lambda 表达式的主体可以包含普通方法可能包含的任何 Java 构造。例如,假设一个字符串作为一个参数被传递给一个 lambda 表达式,并且您想要返回一个依赖于该字符串参数的值。下面的 lambda 表达式主体包含一个代码块,它根据传递给表达式的参数的字符串值返回一个 int。

ActionCode code = (codestr) -> {
    switch(codestr){
        case "ACTIVE": return 0;
        case "INACTIVE": return 1;
        default:
            return -1;
    }
};

在本例中,ActionCode 函数接口用于推断 lambda 表达式的返回类型。为了澄清,让我们看看界面是什么样子的。

interface ActionCode{
    int returnCode(String codestr);
}

代码暗示 lambda 表达式实现了 returnCode 方法,该方法在 ActionCode 接口中定义。这个方法接受一个字符串参数(codestr ),它被传递给 lambda 表达式,返回一个 int。因此,从这个例子中你可以看到 lambda 可以封装方法体的功能。

虽然用 Java 语言编写的代码可以在不使用 lambda 表达式的情况下继续前进,但它们是一个重要的补充,可以极大地提高整体的可维护性、可读性和开发人员的生产率。Lambda 表达式是 Java 语言的一个进化变化,因为它们是语言现代化的又一步,并有助于保持它与其他语言的同步。

注意

lambda 表达式可以包含普通 Java 方法包含的任何语句。然而,continue 和 break 关键字在 lambda 表达式体中是非法的。

6-2.启用 Lambda 表达式的使用

问题

您对创作支持使用 lambda 表达式的代码感兴趣。

解决方案 1

编写可以通过 lambda 表达式实现的自定义函数接口。所有的 lambda 表达式都实现了一个函数接口,也就是带有一个抽象方法声明的接口。下面几行代码演示了一个包含单个方法声明的函数接口。

@FunctionalInterface
interface ReverseType {
    String reverse(String text);
}

函数接口包含一个抽象方法声明,标识为 String reverse(字符串文本)。下面包含 lambda 表达式的代码演示了如何实现 ReverseType。

ReverseType newText = (testText) -> {
    String tempStr = "";
    for (String part : testText.split(" ")) {
        tempStr += new StringBuilder(part).reverse().toString() + " ";
    }
    return tempStr;
};

以下代码可用于调用 lambda 表达式:

System.out.println(newText.reverse("HELLO WORLD"));

结果:

OLLEH DLROW

解决方案 2

使用 java.util.function 包中包含的函数接口来实现 lambda 表达式,以满足应用的需求。以下示例使用函数接口执行与解决方案 1 中演示的任务相同的任务。此示例接受一个字符串参数并返回一个字符串结果。

Function<String,String> newText2 = (testText) -> {
    String tempStr = "";
    for (String part : testText.split(" ")) {
        tempStr += new StringBuilder(part).reverse().toString() + " ";
    }
    return tempStr;
};

这个 lambda 表达式被分配给变量 newText2,它属于函数类型。因此,字符串作为参数传递,并且从 lambda 表达式返回一个字符串。函数的函数接口包含 apply()的抽象方法声明。要调用此 lambda 表达式,请使用以下语法:

System.out.println(newText2.apply("WORLD"));

结果:

DLROW

它是如何工作的

lambda 表达式的基本构件是函数接口。一个函数接口是一个标准的 Java 接口,它包含一个抽象方法声明,并为 lambda 表达式和方法引用提供一个目标类型。一个函数接口也可以包含默认的方法实现,但是只有一个抽象声明。然后,抽象方法由 lambda 表达式隐式实现。因此,lambda 表达式可以赋给与函数接口类型相同的变量。稍后可以从分配的变量调用该方法,从而调用 lambda 表达式。遵循这种模式,lambda 表达式是可以通过名称调用的方法实现。它们也可以作为参数传递给其他方法(见方法 6-9)。

注意

解决方案 1 中的功能接口包含@FunctionalInterface 注释。这可以放在一个函数接口上来捕捉编译器级别的错误,但是它对接口本身没有影响。

此时,您可能想知道是否需要为每种情况开发一个适合 lambda 表达式的函数接口。事实并非如此,因为已经有许多功能接口可供使用。一些例子包括 java.lang.Runnable、javafx.event.EventHandler 和 java.util.Comparator。然而,也有许多不太具体的功能接口,使它们能够被定制以满足特定需求的需要。java.util.function 包包含许多在实现 lambda 表达式时有用的函数接口。软件包中包含的功能接口在整个 JDK 中使用,它们也可以在开发人员应用中使用。表 6-1 列出了 java.util.function 包中包含的功能接口,以及每个接口的描述。请注意,返回布尔值的谓词测试。

表 6-1。java.util.function 中包含的函数接口
|

连接

|

实施描述

| | --- | --- | | 双消费者 | 接受两个输入参数但不返回结果的函数运算。 | | 双功能 | 接受两个参数并产生一个结果的函数。 | | 二元运算符 | 对两个相同类型的操作数进行函数运算,产生与操作数相同类型的结果。 | | 双预测 | 两个参数的谓词。返回一个布尔值。 | | boolean 供应商 | 布尔值结果的提供者。 | | 消费者 | 接受单个输入参数且不返回结果的函数运算。 | | 双重二元运算符 | 对两个双值操作数进行函数运算,并产生一个双值结果。 | | 双重消费者 | 接受单个双值参数且不返回结果的函数运算。 | | 双功能 | 接受双值参数并产生结果的函数。 | | 双重预测 | 一个双值变元的谓词。 | | 双重供应商 | 双值结果的提供者。 | | DoubleToIntFunction | 接受双值参数并产生整数值结果的函数。 | | DoubleToLongFunction | 接受双值参数并产生长值结果的函数。 | | 双元运算符 | 对单个双值操作数进行函数运算,产生双值结果。 | | 功能 | 接受一个参数并产生结果的函数。 | | IntBinaryOperator | 对两个整数值操作数进行函数运算,并产生一个整数值结果。 | | intconsummer | 接受单个整数值参数且不返回结果的函数运算。 | | intfunction | 接受整数值参数并产生结果的函数。 | | intpredictate | 一个整数值参数的谓词。 | | 国际供应商 | int 值结果的提供者。 | | IntToDoubleFunction | 接受 int 值参数并产生 double 值结果的函数。 | | IntToLongFunction | 接受 int 值参数并产生 long 值结果的函数。 | | 插管操作器 | 对单个整数值操作数执行的函数运算,产生一个整数值结果。 | | LongBinaryOperator | 对两个长值操作数进行函数运算,并产生一个长值结果。 | | longconsummer | 接受单个长值参数且不返回结果的函数运算。 | | 长函数 | 接受长值参数并产生结果的函数。 | | 长预测 | 一个长值参数的谓词。 | | 长期供应商 | 长期价值结果的供应商。 | | LongToDoubleFunction | 接受长值参数并产生双值结果的函数。 | | LongToIntFunction | 接受长值参数并产生整数值结果的函数。 | | longunaryooperator | 对单个长值操作数进行函数运算,产生长值结果。 | | object double consumer | 接受一个对象值和一个双值参数但不返回任何结果的函数运算。 | | 对象用户 | 接受一个对象值和一个整数值参数并且不返回任何结果的函数运算。 | | ObjLongConsumer | 接受对象值和长值参数但不返回结果的函数运算。 | | 谓词 | 一个自变量的谓词。 | | 供应商 | 结果的提供者。 | | 到双重功能 | 接受两个参数并产生双值结果的函数。 | | 全功能 | 产生双值结果的函数。 | | 屋顶分叉 | 接受两个参数并产生一个整数值结果的函数。 | | 屋顶功能 | 产生整数值结果的函数。 | | ToLongBiFunction | 接受两个参数并产生长值结果的函数。 | | 托伦函数 | 产生长值结果的函数。 | | 一元运算符 | 对单个操作数进行的函数运算,其结果与其操作数的类型相同。 |

利用 java.util.function 包中包含的函数接口可以大大减少您需要编写的代码量。功能接口不仅面向大部分时间执行的任务,而且还使用泛型编写,允许它们应用于许多不同的上下文。解决方案 2 演示了这样一个例子,其中函数接口用于实现 lambda 表达式,该表达式接受字符串参数并返回字符串结果。

6-3.通过名称调用现有方法

问题

您正在开发一个 lambda 表达式,它仅仅调用一个已经存在于传递给 lambda 的对象中的方法。您希望使用最少的代码,而不是写出调用该方法的整个过程。

解决办法

使用方法引用来调用现有方法,而不是编写 lambda 表达式。在下面的场景中,Player 对象包含一个名为 compareByGoals()的静态方法,该方法接受两个 Player 对象并比较每个对象包含的目标数。然后,它返回一个表示结果的整数。对于所有意图和目的,compareByGoals()方法与比较器相同。

public class Player {

    private String firstName = null;
    private String lastName = null;
    private String position = null;
    private int status = -1;
    private int goals;

    public Player(){

    }

    public Player(String position, int status){
        this.position = position;
        this.status = status;
    }

    public String findPlayerStatus(int status){
        String returnValue = null;

        switch(status){
                case 0:
                        returnValue = "ACTIVE";
                case 1:
                        returnValue = "INACTIVE";
                case 2:
                        returnValue = "INJURY";
                default:
                        returnValue = "ON_BENCH";
        }

        return returnValue;
    }

    public String playerString(){
        return getFirstName() + " " + getLastName() + " - " + getPosition();
    }

    // ** getters and setters removed for brevity **

    /**
     * Returns a positive integer if Player A has more goals than Player B
     * Returns a negative integer if Player A has fewer goals than Player B
     * Returns a zero if both Player A and Player B have the same number of goals
     */
    public static int compareByGoal(Player a, Player b){
        int eval;
        if(a.getGoals() > b.getGoals()){
            eval = 1;
        } else if (a.getGoals() < b.getGoals()){
            eval = -1;
        } else {
            eval = 0;
        }
        return eval;
    }

}

Player.compareByGoal()方法可用于对 Player 对象数组进行排序。为此,将 Player 对象数组(Player[])作为第一个参数传递给 Arrays.sort()方法,并将方法引用 Player::compareByGoal 作为第二个参数传递。结果将是一个按进球数量排序的球员对象列表(升序)。下面一行代码显示了如何完成这项任务。

Arrays.sort(teamArray, Player::compareByGoal);

它是如何工作的

假设您的 lambda 表达式将通过名称调用一个方法,也许会返回一个结果。如果 lambda 表达式符合这种情况,那么它是使用方法引用的主要候选对象。方法引用是 lambda 表达式的简化形式,它指定类名或实例名,后跟要按以下格式调用的方法:

<class or instance name>::<methodName>

双冒号(::)运算符指定方法引用。由于方法引用是一个简化的 lambda 方法,它必须实现一个函数接口,并且接口内的抽象方法必须与被引用的方法具有相同的参数列表和返回类型。任何参数都是从方法引用的上下文中派生出来的。例如,考虑与解决方案相同的场景,您希望通过调用 Player.compareByGoal()方法来执行目标比较,从而对 Player 对象的数组进行排序。可以编写以下代码来通过 lambda 表达式启用此功能:

Arrays.sort(teamArray, (p1, p2) -> Player.compareByGoal(p1,p2));

在这段代码中,数组作为第一个参数传递给 Arrays.sort(),第二个参数是一个 lambda 表达式,它将两个 Player 对象传递给 Player.compareByGoal()方法。lambda 表达式使用函数接口比较器。compare,利用(Player,Player)参数列表。compareByGoal()方法包含相同的参数列表。同样,compareByGoal()的返回类型与函数接口中的返回类型相匹配。因此,不需要在清单中指定参数列表;而是可以从方法引用 Player::compareByGoal 的上下文中推断出来。

有四种不同类型的方法引用,表 6-2 列出了每一种。

表 6-2。方法引用类型
|

类型

|

描述

| | --- | --- | | 静态参考 | 使用对象的静态方法。 | | 实例引用 | 使用对象的实例方法。 | | 任意对象方法 | 用于特定类型的任意对象,而不是特定对象。 | | 构造函数引用 | 用于通过调用带有 new 关键字的构造函数来生成新对象。 |

在该解决方案中,演示了静态方法引用类型,因为 compareByGoal()是 Player 类中的一个静态方法。使用实例引用调用对象实例的方法是可能的。考虑下面的类,它包含一个非静态的方法来比较玩家对象中的目标。

public class PlayerUtility {

    public int compareByGoal(Player a, Player b){
        int eval;
        if(a.getGoals() > b.getGoals()){
            eval = 1;
        } else if (a.getGoals() < b.getGoals()){
            eval = -1;
        } else {
            eval = 0;
        }
        return eval;
    }
}

这个类可以被实例化,新的实例可以用来引用 compareByGoals()方法,类似于这个配方的解决方案中使用的技术。

Player[] teamArray2 = team.toArray(new Player[team.size()]);
PlayerUtility utility = new PlayerUtility();
Arrays.sort(teamArray2, utility::compareByGoal);

假设您的应用包含一个任意类型的列表,并且您想要对该列表中的每个对象应用一个方法。在这种情况下,可以使用方法引用,前提是对象包含可以通过引用使用的方法。在下面的示例中,Arrays.sort()方法应用于 int 值列表,方法引用用于将 Integer compare()方法应用于列表中的元素。因此,结果列表将被排序,方法引用自动传递 int 参数并返回 int 比较。

Integer[] ints = {3,5,7,8,51,33,1};
Arrays.sort(ints, Integer::compare);

最后一种方法引用可用于引用对象的构造函数。当通过工厂创建新对象时,这种类型的方法引用特别有用。让我们看一个例子。假设 Player 对象包含以下构造函数:

public Player(String position, int status, String first, String last){
    this.position = position;
    this.status = status;
    this.firstName = first;
    this.lastName = last;
}

您对使用工厂模式动态生成玩家对象感兴趣。下面的代码演示了一个函数接口的示例,该接口包含一个名为 createPlayer()的抽象方法,该方法接受相同的参数列表作为 Player 对象的构造函数。

public interface PlayerFactory {
    Player createPlayer(String position,
                        int status,
                        String firstName,
                        String lastName);
}

现在可以从 lambda 表达式创建工厂,然后调用它来创建新对象。以下代码行演示了:

PlayerFactory player1 = Player::new;
Player newPlayer = player1.createPlayer("CENTER", 0, "Constructor", "Referenceson");

方法引用可能是 Java 8 中引入的最重要的新特性之一,尽管 lambda 表达式有更多的用例。它们为生成 lambda 表达式提供了一种易读、简化的技术,并且在 lambda 仅仅通过名称调用单个方法的大多数情况下,它们都可以工作。

6-4.使用较少的代码行进行排序

问题

您的应用包含一个曲棍球队的球员对象列表。您希望按照进球最多的球员对球员列表进行排序,并且您希望使用简洁而又易于理解的代码来实现这一点。

注意

这个配方中的解决方案利用了收集和分类。要了解更多关于收藏的信息,请参阅第七章。

解决方案 1

使用 Player 对象中包含的访问器方法为要排序的字段创建一个比较器。在这种情况下,您希望按照目标的数量进行排序,因此比较器应该基于 getGoals()返回的值。下面一行代码展示了如何使用比较器接口和方法引用来创建这样一个比较器。

Comparator<Player> byGoals = Comparator.comparing(Player::getGoals);

接下来,利用 lambda 表达式和流的混合(参见第七章了解关于流的全部细节),以及 forEach()方法,在 Player 对象列表上应用指定的排序。在下面的代码行中,从列表中获得一个流,这允许您对元素应用函数式操作。

team.stream().sorted(byGoals)
                .map(p -> p.getFirstName() + " " + p.getLastName() + " - "
                        + p.getGoals())
                .forEach(element -> System.out.println(element));

假设 team 引用的列表加载了球员对象,前面的代码行将首先按照球员目标对列表进行排序,然后打印出每个对象的信息。

排序的结果:

== Sort by Number of Goals ==
Jonathan Gennick - 1
Josh Juneau - 5
Steve Adams - 7
Duke Java - 15
Bob Smith - 18

解决方案 2

利用 Collections.sort()方法,传递要排序的列表以及对列表元素执行比较的 lambda 表达式。下面的代码演示了如何使用 Collections.sort()技术来完成这项任务。

Collections.sort(team, (p1, p2)
        -> p1.getLastName().compareTo(p2.getLastName()));
team.stream().forEach((p) -> {
    System.out.println(p.getLastName());
});

结果:

== Sort by Last Name ==
Adams
Gennick
Java
Juneau
Smith
注意

如果 Player 类包含一个比较方法,这个解决方案可以进一步简化。如果是这种情况,可以使用方法引用,而不是实现 lambda 表达式。有关方法参考的更多信息,请参见配方 6-4。

它是如何工作的

Java 8 引入了一些新特性,极大地提高了开发人员对集合进行排序的效率。这个配方的解决方案中演示了三个这样的特性:lambda 表达式、方法引用和流。我们将在本书的其他食谱中更详细地研究溪流,但是我们也在这里简要地描述它们以便理解这个食谱。流可以应用于数据集合,它们允许将增强的函数式操作应用于集合中的元素。流不存储任何数据;相反,它们在获取它们的集合上启用了更多的功能。

在解决方案 1 中,生成了一个比较器,通过该比较器将评估玩家对象的进球数(getGoals)。然后从被称为 team 的列表中生成一个流。stream 提供了 sorted()函数,该函数接受一个比较器,通过该比较器对数据流进行排序。最初生成的比较器被传递给 sorted()函数,然后根据结果调用 map()函数。map()函数提供了将表达式应用于流中每个元素的能力。因此,在 map 中,这个解决方案利用 lambda 表达式创建一个字符串,其中包含每个 Player 对象的 firstName、lastName 和 goals 字段。最后,由于 List 是一个 iterable,它包含 forEach()方法。forEach()方法允许将一个表达式或一组语句应用于列表中的每个元素。在这种情况下,列表中的每个元素都被打印到命令行。因此,由于 map()函数被应用于流,所以列表中的每个元素都是按照 map()中应用的算法打印的。因此,结果是球员的名字和姓氏以及每个球员的进球数将在命令行中打印出来。

解决方案 2 使用不同的技术来完成类似的任务。在第二个解决方案中,对列表调用 Collections.sort()方法。Collections.sort()的第一个参数是列表本身,第二个参数是 lambda 表达式形式的比较实现。本例中的 lambda 表达式有两个传递给它的参数,都是 Player 对象,它将第一个玩家的姓氏与第二个玩家的姓氏进行比较。因此,将按照升序对 Player 对象的 lastName 字段进行排序。为了完成解决方案 2,打印出排序后的列表。为此,从排序列表中生成一个流,然后对数据流调用 forEach()方法,打印出每个玩家的姓氏。

毫无疑问,lambda 表达式大大减少了对数据集合进行排序所需的代码量。这也使得理解排序背后的逻辑变得容易,因为可读性比试图遵循过去的循环实现要容易得多。有关使用 lambdas 收集数据的更多示例,请参见第七章。

6-5.过滤数据集合

问题

您有一个数据列表,您希望对其应用一些筛选,以便可以提取符合指定标准的对象。

解决办法

从数据列表中创建一个流,并应用一个过滤器,传递所需的谓词,或者称为条件表达式。最后,将符合指定过滤标准的每个对象添加到新列表中。在下面的例子中,一个球员对象列表被过滤,只捕捉那些已经进了 10 个或更多球的球员。

team.stream().filter(
    p -> p.getGoals() >= 10
    && p.getStatus() == 0)
    .forEach(element -> gteTenGoals.add(element));
System.out.println("Number of Players Matching Criteria: " + gteTenGoals.size());

它是如何工作的

该配方的解决方案利用数据流,因为它包含易于使用的过滤功能。数据集合 team 生成一个流,然后对其调用 filter 函数,接受一个谓词来过滤集合中的数据。谓词以 lambda 表达式的形式编写,包含两个这样的过滤标准。lambda 表达式将一个球员对象作为参数传递,然后根据大于或等于 10 的进球数和活动状态来过滤数据。

过滤完数据后,使用 forEach()方法将每个符合过滤标准的元素添加到列表中。这也是使用 lambda 表达式完成的。要添加到列表中的元素作为参数被传递给 lambda 表达式,随后被添加到表达式主体内的列表中。

Lambda 表达式非常适合在流函数中工作。它们不仅使业务逻辑的开发更容易,而且使集合过滤更容易阅读和维护。

注意

Java 9 中提供了更新的过滤选项,包括 takeWhile 和 dropWhile 构造,这在第二章中有所介绍。详情请见制作方法 2-5。

6-6.实现 Runnable

问题

您希望以简洁的方式创建一段可运行的代码。

解决办法

利用 lambda 表达式实现 java.util.Runnable 接口。java.util.Runnable 接口是 lambda 表达式的完美匹配,因为它只包含一个抽象方法 run()。在这个解决方案中,我们将比较创建新的 Runnable 的遗留技术和使用 lambda 表达式的新技术。

下面几行代码演示了如何使用遗留技术实现一段新的可运行代码。

Runnable oldRunnable = new Runnable() {
    @Override
    public void run() {
        int x = 5 * 3;
        System.out.println("The variable using the old way equals: " + x);
    }
};

现在看看如何用 lambda 表达式来写这个。

Runnable lambdaRunnable = () -> {
    int x = 5 * 3;
    System.out.println("The variable using the lambda equals: " + x);
};

// Calling the runnables

oldRunnable.run();
lambdaRunnable.run();

如您所见,实现 Runnable 的遗留过程比用 lambda 表达式实现 Runnable 多花了几行代码。lambda 表达式还使得 Runnable 实现更容易阅读和维护。

它是如何工作的

因为 java.util.Runnable 是一个函数接口,所以可以使用 lambda 表达式抽象出实现 run()方法的样板文件。用 lambda 表达式实现 Runnable 的一般格式如下:

Runnable assignment = () -> {expression or statements};

Runnable 可以通过使用零参数 lambda 表达式来实现,该表达式包含 lambda 主体中的一个表达式或一系列语句。关键是该实现不带任何参数,也不返回任何内容。

6-7.替换匿名内部类

问题

部分代码包含匿名内部类,这有时很难理解。您希望用更易于阅读和维护的代码替换匿名内部类。

解决办法

用 lambda 表达式替换匿名内部类。通过这样做,开发时间将会更快,因为需要的样板代码将会更少。典型的 JavaFX 或 Java Swing 应用利用匿名内部类向应用结构添加功能。例如,匿名类是向按钮添加动作的好方法。问题是内部类可能很难理解,并且它们包含大量样板代码。

下面几行代码演示了按钮操作实现的典型匿名内部类实现。在了解如何使用 lambda 表达式实现相同的解决方案之前,让我们先看看这几行代码。

Button btn = new Button();
btn.setText("Enter Player");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent e) {
createPlayer(firstName.getText(),
            lastName.getText(),
            Integer.valueOf(goals.getText()),
            listView.getSelectionModel().getSelectedItem().toString(),
            0);
    message.setText("Player Successfully Added");
    System.out.println("Player added.");
    System.out.println("== Current Player List==");
    for (Player p : team) {
        System.out.println(p.getFirstName() + " " + p.getLastName());
    }
}
});

可以使用 lambda 表达式来实现相同的事件处理程序,这样可以用更少的代码实现更易读的实现。

Button btn = new Button();
btn.setText("Enter Player");
btn.setOnAction(e -> {
    createPlayer(firstName.getText(),
                 lastName.getText(),
                 Integer.valueOf(goals.getText()),
                 listView.getSelectionModel().getSelectedItem().toString(),
                 0);
     message.setText("Player Successfully Added");
     System.out.println("Player added.");
     System.out.println("== Current Player List==");
     for (Player p : team) {
         System.out.println(p.getFirstName() + " " + p.getLastName());
     }
});

它是如何工作的

lambda 表达式的一个很好的用例是,它们非常适合代替许多匿名类实现。大多数匿名内部类实现了一个函数接口,这使得它们成为通过 lambda 表达式进行替换的完美候选。在该解决方案中,支持 JavaFX 按钮操作的匿名内部类已经过重新设计,可以在 lambda 表达式的上下文中工作。因为 EventHandler 必须实现一个抽象方法 handle(),所以它非常适合 lambda 实现。

在解决方案中,EventHandler lambda 表达式接受一个参数,该参数的类型是从表达式的上下文中派生的。在这种情况下,由于表达式实现了 EventHandler,因此参数的派生类型是 ActionEvent。lambda 表达式的主体包含几行代码,不向调用者返回任何内容,因为 handle()方法包含一个 void 返回类型。

尽管 lambda 表达式解决方案最多只能保存几行代码,但它确实有助于提高可读性和可维护性。虽然匿名内部类是一个可以接受的解决方案,但是充斥着这种结构的代码使用起来可能会很麻烦。用 lambda 表达式替换匿名内部类有助于维护易于理解的简洁代码。

6-8.从 Lambda 表达式访问类变量

问题

您正在编写的类包含实例变量,并且您希望通过类中的 lambda 表达式来使用它们。

解决办法

根据需要,在 lambda 表达式中使用包含在封闭类中的实例变量。在下面的类中,VariableAccessInner 中包含的 lambda 表达式。InnerClass.lambdaInMethod()方法可以访问所有封闭的类实例变量。因此,如果需要,它能够打印出 VariableAccessInner CLASSA 变量。

public class VariableAccessInner {

    public String CLASSA = "Class-level A";

    class InnerClass {

        public String CLASSA = "Class-level B";

        void lambdaInMethod(String passedIn) {
            String METHODA = "Method-level A";

            Consumer<String> l1 = x -> {
                System.out.println(x);
                System.out.println("CLASSA Value: " + CLASSA);
                System.out.println("METHODA Value: " + METHODA);
            };

            l1.accept(CLASSA);
            l1.accept(passedIn);

        }
    }
}

现在,让我们使用以下代码执行 lambdaInMethod:

VariableAccessInner vai = new VariableAccessInner();
VariableAccessInner.InnerClass inner = vai.new InnerClass();
inner.lambdaInMethod("Hello");

结果:

Class-level B
CLASSA Value: Class-level B
METHODA Value: Method-level A
Hello
CLASSA Value: Class-level B
METHODA Value: Method-level A
注意

CLASSA 变量被 InnerClass 类中使用相同标识符的变量覆盖。因此,不从 lambda 表达式中打印属于 VariableAccessInner 的 CLASSA 实例变量。

它是如何工作的

Lambda 表达式可以访问位于封闭类中的变量。因此,包含在类的方法中的 lambda 表达式可以访问封闭类的任何实例变量。lambda 表达式没有添加额外的作用域,因此它可以访问封闭作用域的字段、方法和局部变量。在该解决方案中,包含在 lambdaInMethod()方法中的 lambda 表达式可以访问在任一类中声明的所有字段。这是因为内部类及其外部类都包含了 lambda。需要注意的一点是,如果内部类包含一个与外部类中声明的变量同名的实例变量,那么 lambda 将使用其封闭类的变量。因此,在该解决方案中,从 lambda 表达式内部访问 InnerClass CLASSA 字段,而不是外部类引用。

从 lambda 表达式中引用的局部变量必须是 final 或有效的 final。因此,如果 lambda 表达式试图访问在封闭方法的上下文中已被更改的变量,将会发生错误。例如,假设解决方案中的方法更改为:

void lambdaInMethod(String passedIn) {
    String METHODA = "Method-level A";
    passedIn = "test";
    Consumer<String> l1 = x -> {
        System.out.println(x);
        System.out.println("CLASSA Value: " + CLASSA);
        System.out.println("METHODA Value: " + METHODA);
        System.out.println(passedIn);
    };

    l1.accept(CLASSA);
    l1.accept(passedIn);

}

注意,就在调用 lambda 表达式之前,传递给 lambdaInMethod()的字符串被赋予一个新值。因此,passedIn 变量实际上不再是 final 变量,lambda 表达式也不能引入新的范围级别。因此,lambda 表达式不能从表达式的上下文中访问 passedIn 变量。

6-9.将 Lambda 表达式传递给方法

问题

创建了一个 lambda 表达式来封装一些功能。您希望将该功能作为参数传递给方法,以便方法实现可以利用表达式。

解决办法

通过实现函数接口,然后将 lambda 表达式赋给与接口类型相同的变量,使用 lambda 表达式创建可移植函数。该变量可以作为参数传递给其他对象。

下面的类 PassingLambdaFunctions 包含 calculate()方法,该方法将用于在给定一组值的情况下执行任何类型的计算。注意,calculate()方法接受一个函数,Double >和一个 Double 值数组作为参数。

public class PassingLambdaFunctions {
    /**
     * Calculates a value based upon the calculation function that is passed
     * in.
     * @param f1
     * @param args
     * @param x
     * @param y
     * @param z
     * @return
     */
    public Double calculate(Function<List<Double>, Double> f1,
                                  Double [] args){
        Double returnVal;
        List<Double> varList = new ArrayList();
        int idx = 0;
        while (idx < args.length){
          varList.add(args[idx]);
          idx++;
        }
        returnVal=f1.apply(varList);

        return returnVal;
    }
}

要使用 calculate 方法,必须将实现函数,Double >的 lambda 表达式作为第一个参数传递给 calculate()方法,同时传递的还有一个 Double 参数数组,其中包含要在计算中使用的值。在下面的类中,使用 lambda 表达式生成一个用于计算体积的函数,并将其分配给函数类型,Double >中标识为 volumeCalc 的变量。另一个 lambda 表达式用于创建计算面积的函数,它被赋给一个相同类型的变量,标识为 areaCalc。在单独的调用中,这些变量随后被传递给 passinglambdafunctions . calculate()方法,以及一个值数组,从而产生计算出的答案。

public class MainClass {
    public static void main(String[] args){

        double x = 16.0;
        double y = 30.0;
        double z = 4.0;

        // Create volume calculation function using a lambda.  The calculator
        // checks to ensure that the array contains the three necessary elements
        // for the calculation.
        Function<List<Double>, Double> volumeCalc = list -> {
            if(list.size() == 3){
                return list.get(0) * list.get(1) * list.get(2);
            } else {
                return Double.valueOf("-1");
            }
        };
        Double[] argList = new Double[3];
        argList[0] = x;
        argList[1] = y;
        argList[2] = z;

        // Create area calculation function using a lambda.  This particular
        // calculator checks to ensure that the array only contains two elements.
        Function<List<Double>, Double> areaCalc = list -> {
            if(list.size() == 2){
                return list.get(0) * list.get(1);
            } else {
                return Double.valueOf("-1");
            }
        };
        Double[] argList2 = new Double[2];
        argList2[0] = x;
        argList2[1] = y;

        PassingLambdaFunctions p1 = new PassingLambdaFunctions();

        // Pass the lambda expressions to the calculate() method, along with the
        // argument lists.
        System.out.println("The volume is: " + p1.calculate(volumeCalc, argList));
        System.out.println("The area is: " + p1.calculate(areaCalc, argList2));
    }
}

结果:

The volume is: 1920.0
The area is: 480.0

它是如何工作的

Lambda 表达式可以分配给与正在实现的函数接口类型相同的变量。这种表达式可以包含单行表达式或多语句体。由于 lambda 表达式可以接受参数,因此存在将这种表达式赋给变量,然后将这些变量传递给其他对象以修改功能的用例。这种模式对于创建可能包含多个实现的解决方案非常有用。这个配方的解决方案演示了这个概念。

在该解决方案中,名为 PassingLambdaFunctions 的类包含一个标识为 calculate()的方法。calculate()方法用于对作为参数传递给它的 Double 值执行计算。但是,calculate()方法不包含任何计算功能。相反,计算功能通过 lambda 表达式作为 Function ,Double >类型的参数传递给它。这种类型实际上是 java.util.function 包中包含的一个标准函数接口(见方法 6-2),该接口可以由 lambda 表达式实现,然后在以后通过调用其 solo apply()方法来调用。查看 calculate()方法中的代码,Double[]中包含的参数首先被添加到一个列表中。接下来,调用 lambda expression 的 apply()方法,传递新的值列表,并将结果返回 returnVal。最后,returnVal 被返回给方法调用程序。

returnVal=f1.apply(varList);
return returnVal;

为了在解决方案中实现计算功能,lambda 表达式是在一个名为 MainClass 的单独类中创建的。每个表达式接受一个参数列表,然后对列表中的值执行计算,并返回一个结果。例如,MainClass 中生成的第一个 lambda 通过将参数列表中包含的所有值相乘来计算体积,并返回结果。然后,这个功能被分配给 Function 、Double >类型的变量,然后它被传递给 passinglambdafunctions . calculate()方法。

任何类型的功能都可以在 lambda 表达式中实现,然后传递给不同的对象使用。这是促进代码重用和高可维护性的一个很好的方法。

摘要

添加到语言中的新构造对 Java 的影响不像 lambda 表达式那么大,这种情况并不常见。多年来,开发人员一直在利用匿名内部类这样的结构为应用添加微妙的功能。随着 lambda 表达式的加入,这种微妙的功能可以用易于阅读的代码来开发,而不是冗余和难以阅读的样板代码。此外,今天的许多语言使得传递功能性代码片段成为可能,动态地改变现有代码的功能。这种解决方案现在可以用 Java 语言实现,允许开发人员利用更现代的编程技术。

Lambda 表达式在 Java 8 中的引入给 Java 语言带来了新的生命,提供了过去 Java 开发人员无法获得的功能。桌面、移动和企业应用的开发人员现在能够利用 lambda 表达式来创建更加健壮和复杂的解决方案。Lambda 表达式是语言的革命性变化,对跨平台开发有着重大影响。

七、数据源和集合

几乎所有应用都针对用户数据执行任务。有时从用户那里获得数据,根据数据执行任务,并立即返回结果。更常见的情况是,先获取数据,然后将数据存储在应用中以备后用,最后根据数据执行任务。应用利用数据结构来存储可以在应用实例的整个生命周期中使用的数据。Java 语言包含许多被称为集合类型的数据结构,它们可以用于这个目的。这些数据结构实现 java.util.Collection 接口,该接口提供了多种方法,可用于添加、移除和执行针对集合所用数据的任务。

谈到数据结构和集合类型,Java 8 改变了游戏规则。引入了管道和流的概念,使得对集合类型中包含的数据进行迭代和操作变得容易。在 Java 的早期版本中,开发人员必须告诉编译器如何迭代集合中的数据。过去,开发人员经常利用循环来对数据结构执行迭代任务。Java 8 使开发人员能够开始利用流来完成集合类型上的迭代任务。当在集合上使用操作的流和管道时,开发人员指定要执行什么类型的操作,而 JDK 决定如何执行。这通过减少样板代码减轻了开发人员的负担,并提供了一种易于使用的处理集合的算法。

本章介绍了一些可以在 Java 应用中用来存储用户数据的数据结构。它详细讨论了一些数据结构,并介绍了可以对数据执行的操作。本章介绍了管道和流的概念,并提供了演示其用法的方法。Java 8 迫使开发人员以不同的方式思考他们编写集合代码的方式,从而开发出更智能、更高效的解决方案。

7-1.定义一组固定的相关常数

问题

您需要一个能够表示一组固定的相关常数的类型。

解决办法

使用枚举类型。以下示例定义了一个名为 FieldType 的枚举类型,以表示您可能在应用的 GUI 上找到的各种表单字段:

// See BasicFieldType.java
public enum FieldType { PASSWORD, EMAIL_ADDRESS, PHONE_NUMBER, SOCIAL_SECURITY_NUMBER }

这是枚举类型的最简单形式,当所需要的只是一组相关的命名常量时,这就足够了。在下面的代码中,声明了一个 FieldType 类型的字段变量,并将其初始化为 FieldType。EMAIL_ADDRESS 枚举常量。接下来,代码打印调用为所有枚举类型定义的各种方法的结果:

FieldType field = FieldType.EMAIL_ADDRESS                                                    ;

System.out.println("field.name(): " + field.name());
System.out.println("field.ordinal(): " + field.ordinal());
System.out.println("field.toString(): " + field.toString());

System.out.println("field.isEqual(EMAIL_ADDRESS): " +
                    field.equals(FieldType.EMAIL_ADDRESS));
System.out.println("field.isEqual(\"EMAIL_ADDRESS\"'): " + field.equals("EMAIL_ADDRESS"));

System.out.println("field == EMAIL_ADDRESS: " + (field == FieldType.EMAIL_ADDRESS));
// Won't compile – illustrates type safety of enum
// System.out.println("field == \”EMAIL_ADDRESS\": " + (field == "EMAIL_ADDRESS"));

System.out.println("field.compareTo(EMAIL_ADDRESS): " +
                    field.compareTo(FieldType.EMAIL_ADDRESS));
System.out.println("field.compareTo(PASSWORD): " + field.compareTo(FieldType.PASSWORD));

System.out.println("field.valueOf(\"EMAIL_ADDRESS\"): " + field.valueOf("EMAIL_ADDRESS"));

try {
    System.out.print("field.valueOf(\"email_address\"): ");
    System.out.println(FieldType.valueOf("email_address"));
} catch (IllegalArgumentException e) {
    System.out.println(e.toString());
}

System.out.println("FieldType.values(): " + Arrays.toString(FieldType.values()));

运行此代码将产生以下输出:

field.name(): EMAIL_ADDRESS
field.ordinal(): 1
field.toString(): EMAIL_ADDRESS
field.isEqual(EMAIL_ADDRESS): true
field.isEqual("EMAIL_ADDRESS"'): false
field == EMAIL_ADDRESS: true
field.compareTo(EMAIL_ADDRESS): 0
field.compareTo(PASSWORD): 1
field.valueOf("EMAIL_ADDRESS"): EMAIL_ADDRESS
field.valueOf("email_address"): java.lang.IllegalArgumentException: No enum constant org.java9recipes.chapter4.BasicEnumExample.FieldType.email_address
FieldType.values(): [PASSWORD, EMAIL_ADDRESS, PHONE_NUMBER, SSN]

它是如何工作的

表示一组固定的相关常数的常见模式是将每个常数定义为 int、String 或其他数据类型。通常,这些常量是在类或接口中定义的,其唯一目的是封装常量。无论如何,常数有时是用 static 和 final 修饰符定义的,如下所示:

// Input field constants
public static final int PASSWORD = 0;
public static final int EMAIL_ADDRESS = 1;
public static final int PHONE_NUMBER = 2;
public static final int SOCIAL_SECURITY_NUMBER = 3;

这种模式有许多问题,主要问题是缺乏类型安全性。通过将这些常量定义为 int,可以将一个无效值赋给一个只允许保存一个常量值的变量:

int inputField = PHONE_NUMBER;  // OK
inputField = 4;  // Bad - no input field constant with value 4; compiles without error

如您所见,不会产生编译器错误或警告来通知您这个无效的值赋值。您可能会在运行时发现这一点,这时您的应用试图使用 inputField,但却给它分配了一个不正确的值。相反,Java 枚举类型提供编译时类型安全。也就是说,如果试图将错误类型的值赋给枚举变量,将会导致编译器错误。在这个配方的解决方案中,字段类型。EMAIL_ADDRESS 枚举常量被分配给字段变量。试图赋一个不属于 FieldType 类型的值自然会导致编译器错误:

FieldType field = FieldType.EMAIL_ADDRESS;  // OK
field = "EMAIL_ADDRESS"; // Wrong type - compiler error

枚举只是一种特殊类型的类。在幕后,Java 实现了一个 enum 类型,作为抽象和最终 java.lang.Enum 类的子类。因此,枚举类型不能直接实例化(在枚举类型之外)或扩展。由枚举类型定义的常量实际上是枚举类型的实例。java.lang.Enum 类定义了许多所有枚举类型都继承的 final 方法。此外,所有枚举类型都有两个隐式声明的静态方法:values()和 valueOf(String)。解决方案代码演示了这些静态方法和一些更常用的实例方法。

这些方法中的大多数都是不言自明的,但是您应该记住以下细节:

  • 每个枚举常量都有一个序数值,表示它在枚举声明中的相对位置。声明中的第一个常量被赋予一个序数值零。ordinal()方法可用于检索枚举常量的序数值;但是,出于可维护性的原因,不建议编写依赖于该值的应用。

  • name()方法和 toString()方法的默认实现都返回枚举常量的字符串表示形式(toString()实际上调用 name())。通常情况下,toString()会被重写,以便为枚举常量提供更加用户友好的字符串表示形式。出于这个原因以及可维护性的原因,建议优先使用 toString()而不是 name()。

  • 在测试相等性时,请注意 equals()方法和==都执行引用比较。它们可以互换使用。但是,建议使用==来利用编译时类型安全。这在解决方案代码中有说明。例如,使用字符串参数执行 equals()比较可能会忽略错误;它会编译,但总是返回 false。相反,试图使用==比较将枚举与字符串进行比较会导致编译时出错。当您可以选择更早(在编译时)捕捉错误而不是更晚(在运行时)捕捉错误时,请选择前者。

  • 隐式声明的静态方法 values()和 valueOf(String)不会出现在 Java 文档或 java.lang.Enum 类的源代码中。然而,Java 语言规范详细说明了它们所需的实现。总结一下这些方法,values()返回一个数组,该数组包含枚举的常数,按照它们的声明顺序排列。valueOf(String)方法返回其名称与 String 参数的值完全匹配(包括大小写)的枚举常量,如果没有指定名称的枚举常量,则引发 IllegalArgumentException。

有关 java.lang.Enum 及其每个方法的更多详细信息,请参考在线 Java 文档(docs . Oracle . com/javase/9/docs/API/Java/lang/enum . html)。正如下一个菜谱所展示的,枚举类型作为成熟的 Java 类,可以用来构建更智能的常量。

7-2.设计智能常数

问题

您需要一个可以表示一组固定的相关常数的类型,并且您希望以面向对象的方式围绕您的常数构建一些状态和行为(逻辑)。

解决办法

使用枚举类型,利用类型安全和枚举类型是成熟的 Java 类这一事实。枚举类型可以像任何其他类一样具有状态和行为,枚举常量本身是枚举类型的实例,它继承了这种状态和行为。这最好用一个例子来说明。让我们扩展一下上一个食谱中的例子。假设您需要处理和验证已提交的 HTML 表单中的所有字段。根据字段类型,每个表单字段都有一组唯一的规则来验证其内容。对于每个表单域,您都有域的“名称”和输入到该表单域中的值。可以扩展 FieldType 枚举来非常容易地处理这个问题:

// See FieldType.java
public enum FieldType {

    PASSWORD(FieldType.passwordFieldName) {

        // A password must contain one or more digits, one or more lowercase letters, one or
        // more uppercase letters, and be a minimum of 6 characters in length.
        //
        @Override
        public boolean validate(String fieldValue) {
            return Pattern.matches("((?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{6,})",
                                   fieldValue);
        }
    },

    EMAIL_ADDRESS(FieldType.emailFieldName) {

        // An email address begins with a combination of alphanumeric characters, periods,
        // and hyphens, followed by a mandatory ampersand ('@') character, followed by
        // a combination of alphanumeric characters (hyphens allowed), followed by a
        // one or more periods (to separate domains and subdomains), and ending in 2-4
        // alphabetic characters representing the domain.
        //
        @Override
        public boolean validate(String fieldValue) {
            return Pattern.matches("^[\\w\\.-]+@([\\w\\-]+\\.)+[A-Z|a-z]{2,4}$",
                                   fieldValue);               
        }
    },

    PHONE_NUMBER(FieldType.phoneFieldName) {

        // A phone number must contain a minium of 7 digits. Three optional digits
        // representing the area code may appear in front of the main 7 digits. The area
        // code may, optionally, be surrounded by parenthesis. If an area code is included,
        // the number may optionally be prefixed by a '1' for long distance numbers.
        // Optional hypens my appear after the country code ('1'), the area code, and the
        // first 3 digits of the 7 digit number.
        //
        @Override
        public boolean validate(String fieldValue) {
            return Pattern.matches("¹?[- ]?\\(?(\\d{3})\\)?[- ]?(\\d{3})[- ]?(\\d{4})$",
                                   fieldValue);
        }
    },

    SOCIAL_SECURITY_NUMBER(FieldType.ssnFieldName) {

        // A social security number must contain 9 digits with optional hyphens after the
        // third and fifth digits.
        //
        @Override
        public boolean validate(String fieldValue) {
            return Pattern.matches("^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$",
                                   fieldValue);
        }

    };  // End of enum constants definition

    // Instance members
    //
    private String fieldName;
    // Define static constants to increase type safety
    static final String passwordFieldName = "password";
    static final String emailFieldName = "email";
    static final String phoneFieldName = "phone";
    static final String ssnFieldName = "ssn";

    private FieldType(String fieldName) {
        this.fieldName = fieldName;
    }

    public String getFieldName() {
        return this.fieldName;
    }

    abstract boolean validate(String fieldValue);
    // Static class members
    //
    private static final Map<String, FieldType> nameToFieldTypeMap = new HashMap<>();

    static {
        for (FieldType field : FieldType.values()) {
            nameToFieldTypeMap.put(field.getFieldName(), field);
        }
    }

    public static FieldType lookup(String fieldName) {
        return nameToFieldTypeMap.get(fieldName.toLowerCase());
    }

    private static void printValid(FieldType field, String fieldValue, boolean valid) {
        System.out.println(field.getFieldName() +
                           "(\"" + fieldValue + "\") valid: " + valid);
    }

    public static void main(String... args) {
        String fieldName = FieldType.passwordFieldName;
        String fieldValue = "1Cxy9";  // invalid - must be at least 6 characters
        FieldType field = lookup(fieldName);
        printValid(field, fieldValue, field.validate(fieldValue));

        fieldName = FieldType.phoneFieldName;
        fieldValue = "1-800-555-1234";  // valid
        field = lookup(fieldName);
        printValid(field, fieldValue, field.validate(fieldValue));

        fieldName = FieldType.emailFieldName;
        fieldValue = "john@doe";  // invalid - missing .<tld>
        field = lookup(fieldName);
        printValid(field, fieldValue, field.validate(fieldValue));

        fieldName = FieldType.ssnFieldName;
        fieldValue = "111-11-1111";  // valid
        field = lookup(fieldName);
        printValid(field, fieldValue, field.validate(fieldValue));
    }
}

运行上述代码会产生以下输出:

password("1Cxy9") valid: false
phone("1-800-555-1234") valid: true
email("john@doe") valid: false
ssn("111-11-1111") valid: true

它是如何工作的

请注意,增强的 FieldType 枚举现在定义了一个 fieldName 实例变量和一个带有 fieldName 字符串参数的构造函数,用于初始化该实例变量。每个枚举常量(同样,每个常量都是 FieldType 的实例)都必须用 fieldName 实例化。FieldType 还定义了一个抽象 validate(String)方法,每个枚举常量都必须实现该方法来执行字段验证。这里,每个字段类型的 validate()方法对字段值应用正则表达式匹配,并返回匹配的布尔结果。想象以下表单输入字段对应于我们的 FieldType 实例:

<input type="password" name="password" value=""/>
<input type="tel" name="phone" value=""/>
<input type="email" name="email" value=""/>
<input type="text" name="ssn" value=""/>

输入字段的名称属性的值将用于标识字段类型;在实例化每个 FieldType 枚举常量时,使用了相同的名称。提交表单时,您可以访问每个输入字段的名称以及输入到该字段中的值。您需要能够将字段名映射到 FieldType,并使用输入值调用 validate()方法。为此,声明并初始化了类变量 nameToFieldTypeMap。对于每个 FieldType 枚举常量,nameToFieldTypeMap 存储一个条目,其中字段名作为键,FieldType 作为值。lookup(String)类方法使用这个映射从字段名中查找 FieldType。验证输入值为 john@doe.com 的电子邮件输入字段的代码非常简洁:

// <input type="email" name="email" value="john@doe.com"/>
String fieldName = FieldType.emailFieldName;
String fieldValue = "john@doe.com";
boolean valid = FieldType.lookup(fieldName).validate(fieldValue);

main()方法展示了每个字段类型的验证示例。printValid()方法打印字段名、字段值和字段的验证结果。

这个方法展示了 enum 类型中有更多的潜力,而不仅仅是定义一组命名常量的能力。枚举类型具有普通类的所有功能,还具有允许您创建封装良好的智能常数的附加功能。

7-3.基于指定的值执行代码

问题

您希望根据单个表达式的值执行不同的代码块。

解决办法

如果您的变量或表达式结果是允许的开关类型之一,并且您希望针对与类型兼容的常数测试是否相等,请考虑使用 switch 语句。这些例子展示了使用 switch 语句的各种方法,包括 Java 7 中的一个新特性:切换字符串的能力。首先,让我们玩石头剪子布吧!RockPaperScissors 类显示了两个不同的 switch 语句:一个使用 int 作为 switch 表达式类型,另一个使用 enum 类型。

// See RockPaperScissors.java
public class                                                     RockPaperScissors {

    enum Hand { ROCK, PAPER, SCISSORS, INVALID };

    private static void getHand(int handVal) {
        Hand hand;
        try {
            hand = Hand.values()[handVal - 1];
        }
        catch (ArrayIndexOutOfBoundsException ex) {
            hand = Hand.INVALID;
        }
        switch (hand) {
            case ROCK:
                System.out.println("Rock");
                break;
            case PAPER:
                System.out.println("Paper");
                break;
            case SCISSORS:
                System.out.println("Scissors");
                break;
            default:
                System.out.println("Invalid");  
        }
    }

    private static void playHands                                                    (int yourHand, int myHand) {

        // Rock = 1
        // Paper = 2
        // Scissors = 3

        // Hand combinations:
        // 1,1; 2,2; 3,3 => Draw
        // 1,2 => sum = 3 => Paper
        // 1,3 => sum = 4 => Rock
        // 2,3 => sum = 5 => Scissors
        //
        switch ((yourHand == myHand) ? 0 : (yourHand + myHand)) {
            case 0:
                System.out.println("Draw!");  
                break;
            case 3:
                System.out.print("Paper beats Rock. ");
                printWinner(yourHand, 2);
                break;
            case 4:
                System.out.print("Rock beats Scissors. ");
                printWinner(yourHand, 1);
                break;
            case 5:
                System.out.print("Scissors beats Paper. ");
                printWinner(yourHand, 3);
                break;
            default:
                System.out.print("You cheated! ");
                printWinner(yourHand, myHand);       
        }
    }

    private static void printWinner                                                    (int yourHand, int winningHand) {
        if (yourHand == winningHand) {
            System.out.println("You win!");
        }
        else {
            System.out.println("I win!");
        }
    }

    public static void main                                                    (String[] args) {

        Scanner input = new Scanner(System.in);
        System.out.println("Let's Play Rock, Paper, Scissors");
        System.out.println("  Enter 1 (Rock)");
        System.out.println("  Enter 2 (Paper)");
        System.out.println("  Enter 3 (Scissors)");
        System.out.print("> ");

        int playerHand = input.hasNextInt() ? input.nextInt() : -99;
        int computerHand = (int)(3*Math.random()) + 1;

        System.out.print("Your hand: (" + playerHand + ") ");
        getHand(playerHand);
        System.out.print("My hand: (" + computerHand + ") ");
        getHand(computerHand);
        playHands(playerHand, computerHand);
    }
}

当执行 RockPaperScissors 类时,一个交互式游戏开始,允许用户在键盘上键入输入。用户可以键入与他们想要选择的条目相对应的数字,计算机利用随机数计算来试图击败用户的选择。

Java 7 增加了切换字符串的能力。SwitchTypeChecker 类演示如何使用字符串作为开关表达式类型。isValidSwitchType()方法接受一个类对象,并确定相应的类型是否是可在开关表达式中使用的有效类型。因此,SwitchTypeChecker 使用 switch 语句来同时演示字符串的切换,并显示在 switch 表达式中使用的有效类型:

// See SwitchTypeChecker.java
public class SwitchTypeChecker {

    public static Class varTypeClass(Object o) { return o.getClass(); };
    public static Class varTypeClass(Enum e) { return e.getClass().getSuperclass(); };
    public static Class varTypeClass(char c) { return char.class; };
    public static Class varTypeClass(byte b) { return byte.class; };
    public static Class varTypeClass(short s) { return short.class; };
    public static Class varTypeClass(int i) { return int.class; };
    public static Class varTypeClass(long l) { return long.class; };
    public static Class varTypeClass(float f) { return float.class; };
    public static Class varTypeClass(double d) { return double.class; };
    public static Class varTypeClass(boolean d) { return boolean.class; };

    public void isValidSwitchType(Class typeClass) {
        String switchType = typeClass.getSimpleName();
        boolean valid = true;
        switch (switchType) {
            case "char":
            case "byte":
            case "short":
            case "int":
                System.out.print("Primitive type " + switchType);
                break;
            case "Character":
            case "Byte":
            case "Short":
            case "Integer":
                System.out.print("Boxed primitive type " + switchType);
                break;
            case "String":
            case "Enum":
                System.out.print(switchType);
                break;
            default:  // invalid switch type
                System.out.print(switchType);
                valid = false;
        }
        System.out.println(" is " + (valid ? "" : "not ") + "a valid switch type.");
    }

    public static void main(String[] args) {
        SwitchTypeChecker check = new SwitchTypeChecker();
        check.isValidSwitchType(varTypeClass('7'));
        check.isValidSwitchType(varTypeClass(7));
        check.isValidSwitchType(varTypeClass(777.7d));
        check.isValidSwitchType(varTypeClass((short)7));
        check.isValidSwitchType(varTypeClass(new Integer(7)));
        check.isValidSwitchType(varTypeClass("Java 8 Rocks!"));
        check.isValidSwitchType(varTypeClass(new Long(7)));
        check.isValidSwitchType(varTypeClass(true));        
        check.isValidSwitchType(varTypeClass(java.nio.file.AccessMode.READ));
    }  
}

以下是执行 SwitchTypeChecker 的结果:

Primitive type char is a valid switch type.
Primitive type int is a valid switch type.
double is not a valid switch type.
Primitive type short is a valid switch type.
Boxed primitive type Integer is a valid switch type.
String is a valid switch type.
Long is not a valid switch type.
boolean is not a valid switch type.
Enum is a valid switch type. 

它是如何工作的

switch 语句是一个控制流语句,允许您根据开关表达式的值执行不同的代码块。它类似于 if-then-else 语句,只是 switch 语句只能有一个测试表达式,并且表达式类型被限制为几种不同类型中的一种。当 switch 语句执行时,它根据包含在 switch 语句的 case 标签中的常量来计算表达式。这些案例标签是代码中的分支点。如果表达式的值等于 case 标签常量的值,则控制权将转移到与匹配的 case 标签相对应的代码部分。然后执行从该点开始的所有代码语句,直到到达 switch 语句的结尾或 break 语句。break 语句导致 switch 语句终止,控制权转移到 switch 语句后面的语句。或者,switch 语句可以包含一个默认标签,当没有等同于 switch 表达式值的 case 标签常量时,该标签为 case 提供一个分支点。

switchtype checker isValidSwitchType()方法演示了如何使用字符串作为开关测试表达式。如果您仔细研究 isValidSwitchType()方法,您会发现它是在测试一个类对象是否表示对应于一个有效开关表达式类型的类型。该方法还演示了如何对事例标签进行分组以实现逻辑或条件测试。如果 case 标签没有任何要执行的关联代码,也没有 break 语句,则执行流程会转到包含可执行语句的下一个最近的 case 标签,这样,如果 switch 表达式的结果与任何一个分组的 case 常量匹配,则允许执行公共代码。

RockPaperScissors 类实现了一个命令行石头剪子布游戏,你可以和电脑玩这个游戏。这个类中有两个方法演示 switch 语句。getHand()方法显示了在 switch 表达式中使用 enum 变量。playHands()方法只是想表明,尽管 switch 表达式通常只是一个变量,但它可以是结果属于允许的开关类型之一的任何表达式。在这种情况下,表达式使用一个返回 int 值的三元运算符。

7-4.使用固定大小的数组

问题

您需要一个简单的数据结构,它可以存储固定(可能是大量)的相同类型的数据,并提供快速的顺序访问。

解决办法

考虑使用数组。虽然 Java 提供了更复杂、更灵活的集合类型,但数组类型对于许多应用来说都是有用的数据结构。下面的示例演示了使用数组的简单性。GradeAnalyzer 类提供了一种计算各种与成绩相关的统计数据的方法,如平均成绩、最低成绩和最高成绩。

// See GradeAnalyzer.java
public class GradeAnalyzer {

    // The internal grades array
    private int[] _grades;

    public void setGrades(int[] grades) {
        this._grades = grades;
    }

    // Return cloned grades so the caller cannot modify our internal grades
    public int[] getGrades() {        
        return _grades != null ? _grades.clone() : null;
    }    

    public int meanGrade() {
        int mean = 0;
        if (_grades != null&& _grades.length > 0) {
            int sum = 0;
            for (int i = 0; i < _grades.length; i++) {
                sum += _grades[i];
            }
            mean = sum / _grades.length;
        }
        return mean;
    }

    public int minGrade() {
        int min = 0;
        for (int index = 0; index < _grades.length; index++) {
            if (_grades[index] < min) {
                min = _grades[index];
            }
        }
        return min;
    }

    public int maxGrade() {
        int max = 0;
        for (int index = 0; index < _grades.length; index++) {
            if (_grades[index] > max) {
                max = _grades[index];
            }
        }
        return max;
    }

    static int[] initGrades1() {
        int[] grades = new int[5];
        grades[0] = 77;
        grades[1] = 48;
        grades[2] = 69;
        grades[3] = 92;
        grades[4] = 87;
        return grades;
}

    static int[] initGrades2() {
        int[] grades = { 57, 88, 67, 95, 99, 74, 81 };
        return grades;
}

    static int[] initGrades3() {
        return new int[]{ 100, 70, 55, 89, 97, 98, 82 };
    }

    public static void main(String... args) {

        GradeAnalyzer ga = new GradeAnalyzer();
        ga.setGrades(initGrades1());
        System.out.println("Grades 1:");
        System.out.println("Mean of all grades is " + ga.meanGrade());
        System.out.println("Min grade is " + ga.minGrade());
        System.out.println("Max grade is " + ga.maxGrade());
        ga.setGrades(initGrades2());
        System.out.println("Grades 2:");
        System.out.println("Mean of all grades is " + ga.meanGrade());
        System.out.println("Min grade is " + ga.minGrade());
        System.out.println("Max grade is " + ga.maxGrade());
        ga.setGrades(initGrades3());
        System.out.println("Grades 3:");
        System.out.println("Mean of all grades is " + ga.meanGrade());
        System.out.println("Min grade is " + ga.minGrade());
        System.out.println("Max grade is " + ga.maxGrade());

        Object testArray = ga.getGrades();
        Class testClass = testArray.getClass();
        System.out.println("isArray: " + testClass.isArray());
        System.out.println("getClass: " + testClass.getName());
        System.out.println("getSuperclass: " + testClass.getSuperclass().getName());
        System.out.println("getComponentType: " + testClass.getComponentType());
        System.out.println("Arrays.toString: " + Arrays.toString((int[])testArray));

    }
}

运行此代码将产生以下输出:

Grades 1:
Mean of all grades is 74
Min grade is 48
Max grade is 92
Grades 2:
Mean of all grades is 80
Min grade is 57
Max grade is 99
Grades 3:
Mean of all grades is 84
Min grade is 55
Max grade is 100
isArray: true
getClass: [I
getSuperclass: class java.lang.Object
getComponentType: int
Arrays.toString: [55, 70, 82, 89, 97, 98, 100]

它是如何工作的

Java 数组类型的工作方式与 Java 的 ArrayList(Java 集合框架的一部分)略有不同。Java 数组保存固定数量的数据。也就是说,创建数组时,必须指定它可以容纳多少数据。一旦创建了数组,就不能插入或删除数组项,也不能更改数组的大小。但是,如果您有固定数量(尤其是非常大量)的数据,并且只需要在顺序迭代时处理这些数据,那么数组可能是一个不错的选择。

关于 Java 数组类型,您需要知道的第一件事是它是一个对象类型。所有数组,不管它们包含什么类型的数据,都以 Object 作为它们的超类。数组的元素可以是任何类型,只要所有元素都是同一类型——要么是基元,要么是对象引用。不管数组类型如何,数组的内存总是从应用的堆空间中分配。堆是 JVM 用于动态内存分配的内存区域。

注意

可以创建一个对象数组(Object[])来保存对不同类型对象的引用;但是,不建议这样做,因为这要求您在从数组中检索元素时检查元素的类型并执行显式类型转换。

在 Java 中完整定义一个数组对象有两个步骤:数组变量声明,它指定数组元素类型,数组创建,它为数组分配内存。一旦数组被声明并且内存被分配,它就可以被初始化。有多种方法可以初始化一个数组,这些方法显示在这个配方的解决方案中。如果您预先知道需要在数组中存储什么数据,那么您可以使用一个快捷语法将数组声明、创建和初始化结合在一个步骤中,您将在解决方案中看到这个快捷语法。

让我们遍历 GradeAnalyzer 类,并研究声明、创建、初始化和访问数组的各种方法。首先,注意这个类有一个实例变量来保存要分析的分数:

private int[] _grades;

与所有其他未初始化的对象引用实例变量一样,_grades 数组实例变量会自动初始化为 null。在开始分析成绩之前,您必须设置 _grades 实例变量来引用您想要分析的成绩数据。这是使用 setGrades(int[])方法完成的。一旦 GradeAnalyzer 有了要分析的等级集合,就可以调用 meanGrade()、minGrade()和 maxGrade()方法来计算它们各自的统计数据。这三个方法一起演示了如何迭代数组的元素,如何访问数组的元素,以及如何确定数组可以容纳的元素数量。要确定数组可以容纳的元素数量,只需访问隐式定义的最终实例变量 length,该变量适用于所有数组:

_grades.length

要迭代数组的元素,只需使用 for 循环,其索引变量遍历数组的所有可能索引。数组索引从 0 开始,所以最后一个数组索引总是(_grades.length - 1)。在对数组进行迭代时,可以通过使用数组变量的名称,后跟用括号括起来的当前索引(通常称为数组下标),来访问当前索引处的数组元素:

// From the meanGrade() method:
for (int i = 0; i < _grades.length; i++) {
    sum += _grades[i];
}

或者,增强的 for 循环,也称为 foreach 循环,可用于迭代数组(有关 foreach 循环的更多讨论,请参见配方 7-7):

for (int grade : _grades) {
    sum += grade;
}

请注意,为了确定最小和最大等级,首先使用 java.util.Arrays 类中的实用程序排序方法按自然(升序)顺序对等级进行排序。排序后,最小坡度是数组的第一个元素(在索引 0 处),最大坡度是数组的最后一个元素(在索引长度-1 处)。

解决方案中的三个静态类方法 initGrades1()、initGrades2()和 initGrades3()演示了创建和初始化数组数据的三种不同方式,您将使用这些数据来“播种”GradeAnalyzer。initGrades1()方法声明并创建一个可以保存五个等级的数组(使用 new ),然后手动将每个元素索引处的值设置为一个整数等级值。initGrades2()方法使用特殊的数组初始值设定项语法将数组创建和初始化结合在一行中:

int[] grades = { 57, 88, 67, 95, 99, 74, 81 };

此语法创建一个长度为 7 的数组,并用所示的整数值初始化从索引 0 到索引 6 的元素。请注意,此语法只能在数组声明中使用,因此不允许出现以下情况:

int[] grades;
grades = { 57, 88, 67, 95, 99, 74, 81 }; // won't compile

initGrades3()方法看起来与 initGrades2()非常相似,但略有不同。这段代码创建并返回一个匿名数组:

return new int[]{ 100, 70, 55, 89, 97, 98, 82 };

使用这种语法,可以对数组元素类型使用 new 关键字,但数组的大小没有明确指定。类似于 initGrades2()方法中显示的数组初始值设定项语法,数组大小由初始值设定项括号中给定的元素数量来表示。所以,这段代码再次创建并返回一个长度为 7 的数组。

在计算了三组成绩数据的成绩统计数据后,GradeAnalyzer main()方法的其余部分演示了可用于确定数组类型信息并将数组转换为可打印字符串的各种方法。您会看到代码首先将调用 getGrades()实例方法返回的数组赋给一个名为 testArray 的对象变量:

Object testArray = ga.getGrades();

您可以进行这种赋值,因为如前所述,数组是一个对象。您也可以通过调用 testArray.getSuperclass()的结果看到这一点。对 testArray.getClass()的调用。getName()也很有趣;它返回“I”。左括号表示“我是一个数组类型”,而“I”表示“具有整数的组件类型”调用 testArray.getComponentType()的结果也支持这一点。最后,调用 Arrays.toString(int[])方法,该方法返回数组及其内容的格式良好的字符串表示。请注意,因为 testArray 是一个对象引用,所以对于 Arrays.toString(int[])方法,必须将其转换为 int 数组。(有关可用于数组的其他有用实用方法,请参见 java.util.Arrays 类的 Java 文档。)

如您所见,数组简单易用。有时候这种简单会对你有利。配方 7-6 展示了数组类型的另一种选择,它提供了简单的元素插入和移除:ArrayList 集合类。

7-5.安全地允许类型或方法对各种类型的对象进行操作

问题

您的应用利用了许多不同的对象类型,并且您的类中有一些容器可用于保存这些不同的类型。您对确保您的应用保持无 bug 感兴趣,但是您希望动态地改变特定容器可能包含的对象类型。换句话说,您希望定义一个通用容器,但是能够在每次实例化容器的新实例时指定其类型。

解决办法

利用泛型类型将类型与容器分离。泛型是一种对对象类型进行抽象的方式,而不是显式声明对象或容器的类型。在使用作为 Java 集合框架一部分的接口和类时,您可能会首先遇到泛型类型(download.oracle.com/javase/tutorial/collections/)。集合框架大量使用 Java 泛型。所有集合类型都被参数化,以允许您在实例化时指定集合可以容纳的元素类型。下面的示例代码演示了如何在几种不同的情况下使用泛型。代码中的注释指出了在哪里使用了泛型。

public class MainClass {

    static List<Player> team;

    private static void loadTeam() {
        System.out.println("Loading team...");

        // Use of the diamond operator
        team = new ArrayList<>();
        Player player1 = new Player("Josh", "Juneau", 5);
        Player player2 = new Player("Duke", "Java", 15);
        Player player3 = new Player("Jonathan", "Gennick", 1);
        Player player4 = new Player("Bob", "Smith", 18);
        Player player5 = new Player("Steve", "Adams", 7);

        team.add(player1);
        team.add(player2);
        team.add(player3);
        team.add(player4);
        team.add(player5);

    }

    public static void main(String[] args) {
        loadTeam();

        // Create a list without specifying a type
        List objectList = new ArrayList();
        Object obj1 = "none";
        objectList.add(obj1);

        // Create a List that can be of type that is any superclass of Player
        List<? super Player> myTeam = objectList;
        for (Object p : myTeam) {
            System.out.println("Printing the objects...");
            System.out.println(p.toString());
        }

        // Create a Map of String keys and String values
        Map<String, String> strMap = new HashMap<>();
        strMap.put("first", "Josh");
        strMap.put("last", "Juneau");
        System.out.println(strMap.values());
    }
}
注意

当我们笼统地谈论集合或集合类型时,您可以将其理解为构成 Java 集合框架的那些类型。这包括从集合和映射接口派生的所有类和接口。集合类型通常指从集合接口派生的类型。

它是如何工作的

解决方案代码演示了泛型的一些基本用例。包含在配方源代码中的 GenericsDemo.java 文件中的例子,更详细地展示了泛型在 Java 集合中的使用,以及如何创建泛型。除非您正在开发一个库 API,否则您可能不会创建自己的泛型类型。但是,如果您了解泛型是如何与集合接口和类一起使用的,您将拥有创建自己的泛型类型所需的知识。

关于 Java 泛型,要理解和记住的第一件事是,它们严格来说是一个编译时特性,可以帮助开发人员创建更多类型安全的代码。当代码被编译成字节码时,参数化泛型类型时指定的所有类型信息都会被编译器“清除”。你会看到这被描述为类型擦除。让我们看一个通用集合类型的例子:列表。列表是一个接口,定义如下:

public interface List                                              <E> extends Collection<E> { ... };

这是一种奇怪的语法,尤其是因为没有对象或类型被标识为 E。事实证明,E 被称为类型参数,它是一个占位符,向编译器指示在运行时将一个类型分配给该对象。类型参数通常是大写字母,用于指示正在定义的参数的类型。有许多不同的类型参数需要注意,但是请记住,这些参数仅在定义泛型类型时适用。在大多数情况下,泛型类型仅在开发库或 API 时定义:

  • 电子元件

  • k 键

  • n 数

  • t 型

  • 价值

  • s、U、V 等等—第二、第三和第四种类型

要为列表(或任何集合类型)指定元素类型,只需在声明和实例化对象时将类型名称包含在尖括号中。当您这样做时,您正在指定一个“参数化类型”下面的代码声明了整数列表。声明参数化类型列表的变量 List,然后使用从参数化类型 LinkedList (也称为“具体参数化类型”)的实例化中获得的引用进行初始化:

List<Integer> aList = new LinkedList<Integer>();

既然已经参数化了这些类型,将元素类型限制为整数,那么 List add(E e)方法就变成了:

boolean add(Integer e);

如果试图向列表中添加整数以外的任何内容,编译器将会生成错误:

aList.add(new Integer(121));
aList.add(42);   // 42 is the same as new Integer(42), due to autoboxing.
aList.add("Java");  // won't compile, wrong type

需要注意的是,编译时检查的是引用类型,所以下面的代码也会导致编译器错误:

Number aNum = new Integer("7");
aList.add(aNum);  // won't compile, wrong type

这是一个编译错误,因为 aNum 可以引用任何数字对象。如果编译器允许这样做,您可能会得到一个包含 Doubles、Floats 等的集合,这将违反创建 List 时指定的整数参数约束。当然,简单的类型转换可以避免编译器错误,但是当在不兼容的数字对象之间转换时,这肯定会导致意想不到的后果。泛型旨在减少您必须在代码中进行的显式类型转换的数量,因此如果您发现自己在使用参数化类型的方法时使用了显式类型转换,这是潜在危险代码的线索。

aList.add((Integer)aNum);  // compiles, but don't do this.

使用泛型类型时要注意的其他事情是编译器警告。它们可能表明您正在做一些不被推荐的事情,并且通常表明您的代码有潜在的运行时错误。一个例子可以帮助说明这一点。下面的代码可以编译,但会产生两个编译器警告:

List rawList                                               = new LinkedList();
aList = rawList;

首先,创建 rawList,这是一个原始类型,一个没有参数化的泛型类型。当泛型被引入语言时,语言设计者决定为了保持与前泛型代码的兼容性,他们需要允许使用原始类型。然而,对于较新的(Java 5 之后)代码,强烈建议不要使用原始类型,因此如果您使用它们,编译器会生成一个原始类型警告。接下来,rawList 被赋值给一个使用参数化类型创建的 List。同样,这是编译器允许的(由于泛型类型擦除和向后兼容性),但会为赋值生成未经检查的转换警告,以标记潜在的运行时类型不兼容。想象一下如果罗尔斯主义包含了字符串。后来,如果您试图从一个列表中检索整数元素,您将得到一个运行时错误。

关于类型兼容性,它不适用于泛型类型参数。例如,以下是无效的赋值:

List<Number> bList = new LinkedList<Integer>();  // won't compile; incompatible types

虽然整数是数字(Integer 是 Number 的子类型),LinkedList 是 List 的子类型,但是 LinkedList 不是 List 的子类型。幸运的是,如果你不小心编写了这样的代码,这不会从你身边溜走;编译器将生成“不兼容类型”警告。

因此,您可能想知道是否有一种方法可以实现变体子类型关系,类似于我们在前一行代码中尝试做的事情。答案是肯定的,通过使用称为通配符的泛型特性。通配符用问号(?)在类型参数尖括号内。通配符用于声明有界或无界的参数化类型。下面是一个有界参数化类型的示例声明:

List<? extends Number> cList;

当通配符与 extends 关键字一起使用时,会为类型参数建立一个上限。在这个例子中?extends Number 指的是数字或数字的子类型的任何类型。因此,以下是有效的赋值,因为 Integer 和 Double 都是 Number 的子类型:

cList = new LinkedList<Number>();
cList = new LinkedList<Integer>();
cList = new LinkedList<Double>();

因此,cList 可以保存对任何具有与 Number 兼容的元素类型的列表实例的引用。事实上,cList 甚至可以引用原始类型。显然,如果编译器允许将元素添加到 cList 中,这就给它带来了强制类型安全的挑战。因此,编译器不允许将元素(null 元素除外)添加到用?延伸。以下内容会导致编译器错误:

cList.add(new Integer(5));  // add() not allowed; cList could be LinkedList<Double>

但是,您可以毫无问题地从列表中获取元素:

Number cNum = cList.get(0);

这里唯一的限制是从列表中得到的引用必须像数字一样对待。记住,cList 可能指向一个整数列表、一个双精度数列表或任何其他数字子类型的列表。

通配符也可以与 super 关键字一起使用。在这种情况下,会为类型参数建立一个下限:

List<? super Integer> dList;

在这个例子中?超整数是指整数或整数的任何超类型。因此,以下是有效的赋值,因为 Number 和 Object 是 Integer 的唯一超类型:

dList = new LinkedList<Integer>();
dList = new LinkedList<Number>();
dList = new LinkedList<Object>();

所以,你看那个整数是下限。这个下限现在限制了从列表中检索元素。因为 dList 可以保存对任何一个以前的参数化类型的引用,所以如果对要检索的元素的类型作出假设,编译器将无法实施类型安全。因此,编译器一定不允许对用?super,以下内容将导致编译器错误:

Integer n = dList.get(0);  // get() not allowed; dList.get(0) could be a Number or Object

然而,现在您可以向列表中添加元素,但是下限 Integer 仍然适用。只能添加整数,因为整数与数字和对象兼容:

dList.add(new Integer(5));  // OK
Number dNum = new Double(7);
dList.add(dNum);  // won't compile; dList could be LinkedList<Integer>

您将看到通配符在整个集合类型中与 extends 和 super 一起使用。最常见的是,您会看到它们被用在方法参数类型中,例如 addAll()方法,它是为所有集合定义的。有时您会看到使用通配符(?)单独作为类型参数,这称为无界通配符。集合 removeAll()方法就是这样一个例子。在大多数情况下,这种用法不言自明。您可能不会(可能不应该)使用无限通配符定义自己的参数化类型。如果你尝试这样做,你很快就会发现你用它做不了什么。如果您理解了具体的参数化类型、通配符参数化类型以及有界和无界类型的概念(如本食谱中所述),您就拥有了使用泛型集合类型所需的大部分内容,并且如果您愿意,还可以创建自己的泛型类型。

既然我们已经谈了很多关于参数化类型的内容,我们将告诉你忘掉其中的一些。当 Java 7 发布时,引入了一个新的特性,叫做 diamond(有时被称为 diamond 操作符,尽管在 Java 中它不被认为是一个操作符)。菱形允许编译器从参数化类型用法的上下文中推断类型变量。钻石用法的一个简单例子如下:

List<Integer> eList = new ArrayList<>();

注意,在实例化 ArrayList 时,尖括号之间没有指定类型参数。根据赋值或初始化的上下文,编译器可以很容易地推断出该类型是整数。Integer 是唯一适用于这种环境的类型。事实上,如果你没有在可能的地方使用菱形,Java 编译器(和大多数兼容的 ide)会警告你。另一个更复杂的例子更好地展示了好处:

Map<Integer, List<String>> aMap = new HashMap<>();  // Nice!

菱形同样可以用在 return 语句以及方法参数中:

// diamond in method return
public static List<String> getEmptyList() {
    return new ArrayList<>();
}

// diamond in method argument
List<List<String>> gList = new ArrayList<>();
gList.set(0, new ArrayList<>(Arrays.asList("a", "b")));

请注意,使用这里显示的菱形与使用原始类型是不同的。以下不等同于使用钻石的 aMap 的声明;它将导致编译器发出“未检查的转换”警告,并且可能是原始类型警告:

Map<Integer, List<String>> bMap                                               = new HashMap();   // compiler warnings; avoid raw types

关于为什么这与钻石的例子不同的讨论超出了本食谱的范围。如果你记得避免使用原始类型,你应该不需要担心这一点。只要有可能,就使用菱形,这样可以节省一些打字的时间,并使代码更加健壮、易读和简洁。

7-6.使用动态数组

问题

您需要一个灵活的数据结构,可以存储可变数量的数据,并允许轻松地插入和删除数据。

解决办法

考虑使用数组列表。下面的示例代码是 StockScreener 类,它允许您根据特定的筛选参数(P/E、Yield 和 Beta)和筛选值来筛选股票列表或单只股票。该类利用数组列表来包含股票字符串。一个示例屏幕可能是“告诉我这个列表中的哪些股票的 P/E(市盈率)为 15 或更低。”如果你不熟悉这些股票市场术语,不要担心。无论你做什么,不要用这个类来做你的股票投资决策!

// See StockScreener.java
public class StockScreener {

    enum Screen { PE, YIELD, BETA };

    public static boolean screen                                                    (String stock, Screen screen, double threshold) {
        double screenVal = 0;
        boolean pass = false;
        switch (screen) {
            case PE:
                screenVal = Math.random() * 25;
                pass = screenVal <= threshold;
                break;
            case YIELD:
                screenVal = Math.random() * 10;
                pass = screenVal >= threshold;
                break;
            case BETA:
                screenVal = Math.random() * 2;
                pass = screenVal <= threshold;
                break;
        }
        System.out.println(stock + ": " + screen.toString() + " = " + screenVal);

        return pass;
    }

    /**
     * Parse through stock listing to determine if each stock passes the screen tests.  If
     * a particular element does not pass the screen, then remove it.
     */
    public static void screen                                                    (List<String> stocks, Screen screen, double threshold) {
        Iterator<String> iter = stocks.iterator();
        while (iter.hasNext()) {
            String stock = iter.next();
            if (!screen(stock, screen, threshold)) {
               iter.remove();
            }
        }
    }

    public static void main                                                    (String[] args) {

        List<String> stocks = new ArrayList<>();
        stocks.add("ORCL");
        stocks.add("AAPL");
        stocks.add("GOOG");
        stocks.add("IBM");
        stocks.add("MCD");
        System.out.println("Screening stocks: " + stocks);

        if (stocks.contains("GOOG") &&
            !screen("GOOG", Screen.BETA, 1.1)) {
            stocks.remove("GOOG");
        }
        System.out.println("First screen: " + stocks);

        StockScreener.screen(stocks, Screen.YIELD, 3.5);
        System.out.println("Second screen: " + stocks);
        StockScreener.screen(stocks, Screen.PE, 22);
        System.out.println("Third screen: " + stocks);

        System.out.println("Buy List: " + stocks);   
    }
}

运行这段代码的输出会有所不同,因为它是随机分配股票的屏幕结果值。下面是运行该类的一个输出示例:

Screening stocks: [ORCL, AAPL, GOOG, IBM, MCD]
GOOG: BETA = 1.9545048754918146
First screen: [ORCL, AAPL, IBM, MCD]
ORCL: YIELD = 5.54002319921808
AAPL: YIELD = 5.282200818124754
IBM: YIELD = 3.189521157557543
MCD: YIELD = 3.978628208965815
Second screen: [ORCL, AAPL, MCD]
ORCL: PE = 3.5561302619951993
AAPL: PE = 13.578302484429233
MCD: PE = 23.504349376296886
Third screen: [ORCL, AAPL]
Buy List: [ORCL, AAPL]

它是如何工作的

ArrayList 是 Java 集合框架中最常用的类之一。ArrayList 类实现 List 接口,而 List 接口又实现 Collection 接口。集合接口为所有集合类型定义了一组公共操作,列表接口定义了一组特定于面向列表的集合类型的操作。集合框架大量使用 Java 泛型。如果你是泛型的新手,建议你阅读食谱 7-5,它给出了泛型的简要概述和它们在集合中的使用。

StockScreener main()方法首先声明一个股票列表,并使用泛型类型参数指定股票列表元素的类型为 String。注意,实际的列表类型是一个用菱形创建的数组列表,这将在 7-5 中讨论。股票列表将包含数量可变的股票,由股票市场符号(字符串)表示:

List<String> stocks = new ArrayList<>();

既然您已经指定了股票列表只能保存字符串,那么所有的列表方法都被参数化为只允许字符串。因此,接下来,代码多次调用 ArrayList 的 add(String)方法将股票添加到列表中。之后,在谷歌上运行一个基于 Beta(衡量股票风险的一种方法)的屏幕;如果没有通过屏幕,则调用 List remove(String)方法从股票列表中删除股票。然后对整个股票列表再进行两次筛选,以获得市盈率为 22.0 或更低、收益率为 3.5%或更高的股票列表。用于这些屏幕的 screen()方法接受一个 List 类型的参数。它必须遍历列表,对列表中的每只股票进行筛选,并删除那些没有通过筛选的股票。注意,为了在迭代集合时安全地从集合中移除元素,必须使用集合的迭代器来使用 iterate,迭代器可以通过调用 Iterator()方法来获得。这里,我们展示了 while 循环在股票列表上的使用(for 循环也可以类似地使用)。只要没有到达列表的末尾(iter.hasNext()),就可以从列表中获取下一只股票(iter.next()),运行屏幕,如果屏幕没有通过,就从列表中删除元素(iter.remove())。

注意

您可能会发现在迭代列表时调用列表的 remove()方法似乎很有效。问题是它不能保证有效,而且会产生意想不到的结果。在某些时候,代码还会抛出一个 ConcurrentModificationException,不管是否有多个线程访问同一个列表。记住,在迭代任何集合时,总是通过迭代器移除元素。

ArrayList 是一种非常有用的数据结构,通常应该用来代替 array 类型。它提供了比简单数组更大的灵活性,因为可以轻松地动态添加和删除元素。虽然 ArrayList 确实在内部使用了一个数组,但是您可以从为您实现的优化的 add()和 remove()操作中受益。此外,ArrayList 实现了许多其他非常有用的方法。有关更多详细信息,请参考在线 Java 文档(docs . Oracle . com/javase/9/docs/API/Java/util/ArrayList . html)。

7-7.使你的对象可迭代

问题

您已经创建了一个基于自定义集合的类,该类包装(而不是扩展)了基础集合类型。在不公开类的内部实现细节的情况下,您希望类的对象变得可迭代,尤其是在使用 foreach 语句的情况下。

解决办法

让您的类扩展 Interable 接口,其中 T 是要迭代的集合的元素类型。实现 iterator()方法以从集合中返回迭代器对象。这个食谱的例子是 StockPortfolio 类。在内部,StockPortfolio 管理一组股票对象。我们希望我们类的用户能够使用 foreach 语句将 StockPortfolio 对象视为 iterable 对象。StockPortfolio 类如下:

// See StockPortfolio.java and Stock.java
public class StockPortfolio                                                     implements Iterable<Stock> {

    Map<String, Stock> portfolio = new HashMap<>();

    public void add(Stock stock) {
        portfolio.put(stock.getSymbol(), stock);
    }

    public void add(List<Stock> stocks) {
        for (Stock s : stocks) {
            portfolio.put(s.getSymbol(), s);
        }
    }

    @Override
    public Iterator<Stock> iterator() {
        return portfolio.values().iterator();
    }

    public static void main(String[] args) {

        StockPortfolio myPortfolio = new StockPortfolio();
        myPortfolio.add(new Stock("ORCL", "Oracle", 500.0));
        myPortfolio.add(new Stock("AAPL", "Apple", 200.0));
        myPortfolio.add(new Stock("GOOG", "Google", 100.0));
        myPortfolio.add(new Stock("IBM", "IBM", 50.0));
        myPortfolio.add(new Stock("MCD", "McDonalds", 300.0));

        // foreach loop (uses Iterator returned from iterator() method)
        System.out.println("====Print using legacy for-each loop====");
        for (Stock stock : myPortfolio) {
            System.out.println(stock);
        }
        System.out.println("====Print using Java 8 foreach implementation====");
        myPortfolio.forEach(s->System.out.println(s));
    }
}

以下代码是股票类的代码:

public class Stock {
    private String symbol;
    private String name;
    private double shares;
    public Stock(String symbol, String name, double shares) {
        this.symbol = symbol;
        this.name = name;
        this.shares = shares;
    }
    public String getSymbol() {
        return symbol;
    }
    public String getName() {
        return name;
    }
    public double getShares() {
        return shares;
    }
    public String toString() {
        return shares + " shares of " + symbol + " (" + name + ")";
    }
}

main()方法创建一个 StockPortfolio,然后调用 add()方法将一些股票添加到投资组合中。然后,foreach 循环的两种变体(legacy 和 forEach 实现)用于循环并打印投资组合中的所有股票。运行 StockPortfolio 类会产生以下输出:

50.0 shares of IBM (IBM)
300.0 shares of MCD (McDonalds)
100.0 shares of GOOG (Google)
200.0 shares of AAPL (Apple)
500.0 shares of ORCL (Oracle)
注意

在您的环境中运行 StockPortfolio 类时,输出中各行的顺序可能会有所不同,因为底层实现使用了 HashMap。HashMap 不保证存储在 Map 中的元素的顺序,这扩展到了它的迭代器。如果希望迭代器返回按股票符号排序的元素,可以使用排序集合之一,比如 TreeMap 或 TreeSet,而不是 HashMap。另一种选择是在集合上利用流。有关流的更多信息,请参见配方 7-10。

它是如何工作的

Iterable 接口是在 Java 5 中引入的,以支持同时引入的增强的 for 循环(也称为 foreach 循环)。除了这些对语言的增强,所有的集合类都被改进以实现 iterable 接口,从而允许使用 foreach 循环来实现集合类的 Iterable。Iterable 接口是一种泛型类型,定义如下:

public interface Iterable<T> {
    Iterator<T> iterator();
}

任何实现 Iterable 的类都必须实现 iterator()方法来返回 Iterator 对象。通常,返回的迭代器是底层集合的默认迭代器;然而,它也可能返回一个自定义迭代器的实例。在 StockPortfolio 类中,地图用于表示股票投资组合。每个映射条目的键是股票符号,与每个键相关联的值是股票对象。Java 中的映射是不可迭代的;也就是说,它们不是集合类。因此,它们不实现 Iterable。然而,映射的键和值都是集合,因此是可迭代的。我们希望 Iterable iterator()方法的实现返回投资组合图的值(股票引用)的迭代器;因此,我们的 Iterable 实现由股票类型参数化:

public class StockPortfolio implements Iterable<Stock>

Map values()方法返回地图值的集合;在这种情况下,股票的集合。iterator()方法实现可以简单地返回集合的迭代器:

@Override
public Iterator<Stock> iterator() {
    return portfolio.values().iterator();
}

使用 Iterable 的这个实现,可以使用传统的 foreach 循环或 forEach 实现来迭代 StockPortfolio 实例并打印每只股票:

myPortfolio.forEach(s->System.out.println(s));

随着 Java 8 的发布,forEach 方法是 Iterable 接口的新功能。该方法对 Iterable 中的每个元素执行指定的操作,直到处理完所有元素,否则指定的操作将引发异常。在这个解决方案中,指定的动作是一个 lambda 表达式(参见第六章),它打印 myPortfolio Iterable 中每个元素的值。

您会注意到 StockPortfolio 也包含 add(List )方法,该方法允许从列表中填充投资组合。该方法还使用 foreach 循环来遍历输入列表。同样,这是可能的,因为列表是可迭代的。(注意,代码中从不调用这个方法;它的存在只是为了说明的目的。)

注意

我们实施股票投资组合有一个问题。我们已经竭尽全力不公开我们的类的内部实现细节(组合图)。这允许我们在不影响 StockPortfolio 客户端代码的情况下更改实现。然而,当我们实现 Iterable 时,我们通过 iterator()方法有效地导出了底层的项目组合图。正如方法 7-5 所演示的,迭代器允许通过调用它的 remove()方法来修改底层集合。不幸的是,Java 没有提供可以用来包装迭代器并防止修改底层集合的 UnmodifiableIterator 类。然而,实现这样一个类是很简单的,它将 hasNext()和 Next()调用转发给包装的迭代器,但不实现 remove()方法(根据迭代器 Java 文档,应该抛出 UnsupportedOperationException)。或者,您的 iterator()方法可以从通过调用 collections . unmodifiablecollection()类方法获得的不可修改集合中返回迭代器。我们鼓励您探索这两个选项。首先,源代码下载中提供了一个可能的 UnmodifiableIterator 实现(参见 UnmodifiableIterator.java)。

正如您在这个配方中看到的,iterable 接口允许您创建与 foreach 实现兼容的 Iterable 对象。当您想要设计一个封装实现细节的基于自定义集合的类时,这非常有用。请记住,为了加强封装并防止底层集合被修改,您应该实现前面提到的解决方案之一。

7-8.迭代集合

问题

您的应用包含集合类型,并且您想要迭代其中的元素。

解决办法

在扩展或实现 java.util.Collection 的任何类型上生成流,然后在集合的每个元素上执行所需的任务。在下面的代码中,加载了 Stock 对象的 ArrayList 用于演示流的概念。

public class StreamExample {
    static List<Stock> myStocks = new ArrayList();

    private static void createStocks(){
        myStocks.add(new Stock("ORCL", "Oracle", 500.0));
        myStocks.add(new Stock("AAPL", "Apple", 200.0));
        myStocks.add(new Stock("GOOG", "Google", 100.0));
        myStocks.add(new Stock("IBM", "IBM", 50.0));
        myStocks.add(new Stock("MCD", "McDonalds", 300.0));
    }

    public static void main(String[] args){
        createStocks();
        // Iterate over each element and print the stock names
        myStocks.stream()
                .forEach(s->System.out.println(s.getName()));

        boolean allGt = myStocks.stream()
                .allMatch(s->s.getShares() > 100.0);
        System.out.println("All Stocks Greater Than 100.0 Shares? " + allGt);

        // Print out all stocks that have more than 100 shares
        System.out.println("== We have more than 100 shares of the following:");
        myStocks.stream()
                .filter(s -> s.getShares() > 100.0)
                .forEach(s->System.out.println(s.getName()));

        System.out.println("== The following stocks are sorted by shares:");
        Comparator<Stock> byShares = Comparator.comparing(Stock::getShares);
        Stream<Stock> sortedByShares = myStocks.stream()
                .sorted(byShares);
        sortedByShares.forEach(s -> System.out.println("Stock: " + s.getName() + " - Shares: " + s.getShares()));

        // May or may not return a value
        Optional<Stock> maybe = myStocks.stream()
                .findFirst();
        System.out.println("First Stock: " + maybe.get().getName());

        List newStocks = new ArrayList();
        Optional<Stock> maybeNot = newStocks.stream()
                .findFirst();
        Consumer<Stock> myConsumer = (s) ->
        {
          System.out.println("First Stock (Optional): " + s.getName());
        };
        maybeNot.ifPresent(myConsumer);

        if(maybeNot.isPresent()){
            System.out.println(maybeNot.get().getName());
        }

        newStocks.add(new Stock("MCD", "McDonalds", 300.0));
        Optional<Stock> maybeNow = newStocks.stream()
                .findFirst();
        maybeNow.ifPresent(myConsumer);
    }

}

执行这段代码的结果演示了使用流的概念。外部迭代(对于循环)不再是迭代数据集合的必要条件。

它是如何工作的

在 Java 8 之前,迭代一个集合需要某种循环块。这就是所谓的外部迭代,也就是按顺序的程序循环。在大多数情况下,for 循环用于遍历集合中的每个元素,根据应用的要求处理每个元素。虽然 for 循环是执行迭代的合理解决方案,但它是一种不直观且冗长的策略。自从 Java 8 发布以来,迭代集合的样板文件被删除了,同时还删除了说明如何完成迭代的要求。编译器已经知道如何迭代一个集合,那么为什么还要告诉编译器具体怎么做呢?为什么不简单地告诉编译器:“我想迭代这个集合,并对每个元素执行这个任务”?流的概念支持这种不干涉迭代的方法。

让编译器处理非直觉循环,简单地把任务交给编译器,告诉它对每个元素执行什么操作。这个概念被称为内部迭代。使用内部迭代,您的应用确定需要迭代什么,JDK 决定如何执行迭代。内部迭代不仅减轻了对循环逻辑编程的需求,而且还有其他优点。一个这样的优点是内部迭代不限于对元素的顺序迭代。因此,JDK 决定如何迭代,为手头的任务选择最佳算法。内部迭代也可以更容易地利用并行计算。这个概念包括将任务细分成更小的问题,同时解决每一个问题,然后合并结果。

流是可以在所有集合类型上生成的对象引用序列。Stream API 可以对这些对象引用执行一系列聚合操作,或者返回结果,或者以内联方式将更改应用到对象。这也称为管道。生成和使用流的伪代码如下:

Collection -> (Stream) -> (Zero or More Intermediate Operations) -> (Terminal Operation)

让我们把这个伪代码放到一个真实的例子中。在该解决方案中,一个股票对象列表用于演示流迭代。假设您想打印出每只股票,它包含的股票数量超过了指定的阈值(在本例中为 100 股)。您可以使用以下代码来执行此任务:

myStocks.stream()
                .filter(s -> s.getShares() > 100.0)
                .forEach(s->System.out.println(s.getName()));

在前面的示例中,一个称为 filter()的中间操作用于对元素进行限制,从而过滤掉所有与所提供的谓词不匹配的元素。谓词以 lambda 表达式的形式编写;它对每个元素执行测试,并返回一个布尔结果。示例中的 terminal 操作使用 forEach()打印每个匹配的元素。终结操作是管道中的最后一个操作,它产生非流结果,如原语、集合或根本没有值。在示例情况下,不返回任何结果。

若要在集合类型上生成流,请调用 stream()方法,该方法将返回流类型。在大多数情况下,流类型不是期望的结果,因此流 API 使得在流上调用零个或多个中间操作成为可能,从而形成操作管道。例如,在解决方案中,使用下面的代码按照股票数量对股票对象列表进行排序。注意,比较器 byShares 应用于流中的每个对象,结果返回流:

Stream<Stock> sortedByShares = myStocks.stream()
                .sorted(byShares);

在前面的示例中,对流执行了一个中间操作 sorted()。如前所述,可能有一个以上的中间操作链接到该管道,从而对满足前一个操作的标准的那些对象执行下一个操作。每个中间操作都返回一个流。每个管道可以包含一个终端操作,从而将终端操作应用于每个结果流对象。如前所述,终端操作可能会也可能不会返回结果。在前面的示例中,没有应用任何终端操作。

注意

Stream 的在线文档(docs . Oracle . com/javase/9/docs/API/Java/util/Stream/Stream . html)列出了流上可用的所有中间和终端操作。

对于 Java 编程语言来说,流是一个革命性的变化。它们改变了开发人员思考程序的方式,使开发人员更有生产力,代码更有效率。虽然诸如 for 循环之类的遗留迭代技术仍然被认为是有效的过程,但是当您使用 Java 8 或更高版本时,流是迭代的首选技术。

7-9.在地图上迭代

问题

您正在使用一个 Map 类,比如 HashMap 或 TreeMap,您需要迭代键、值或两者。您还希望在对地图进行迭代时从地图中移除元素。

解决办法

有多种方法可以迭代地图。您选择的方法应该取决于您需要访问地图的哪些部分,以及在迭代时是否需要从地图中移除元素。StockPortfolio1 类是上一个菜谱中显示的 StockPorfolio 类的延续。它添加了三个方法 summary()、alertList()和 remove(List ),演示了迭代投资组合图的替代方法:

// See StockPortfolio1.java
Map<String, Stock> portfolio = new HashMap<>();
...
public void summary() {
    System.out.println("==Legacy technique for traversing Map.Entry==");
    for (Map.Entry<String, Stock> entry : portfolio.entrySet()) {
        System.out.println("Stock = " + entry.getKey() + ", Shares = " + entry.getValue().getShares());
    }

    System.out.println("==Utilization of new foreach and lambda combination==");
    portfolio.forEach((k,v)->System.out.println("Stock = " + k + ", Shares = " + v.getShares()));
}

/**
 * Utilize for loop to traverse Map keys and apply filter to obtain desired
 * stocks
 * @return
 */
public List<Stock> alertListLegacy() {
    System.out.println("==Legacy technique for filtering and collecting==");
    List<Stock> alertList = new ArrayList<>();
    for (Stock stock : portfolio.values()) {
        if (!StockScreener.screen(stock.getSymbol(), StockScreener.Screen.PE, 20)) {
            alertList.add(stock);
        }
    }

    return alertList;
}

/**
 * Utilize stream and filters to obtain desired stocks
 * @return
 */
public List<Stock> alertList(){
    return
    portfolio.values().stream()
            .filter(s->!StockScreener.screen(s.getSymbol(), StockScreener.Screen.PE, 20))
            .collect(Collectors.toList());

}

public void remove(List<String> sellList) {
    Iterator<String> keyIter = portfolio.keySet().iterator();
    while (keyIter.hasNext()) {
        if (sellList.contains(keyIter.next())) {
            keyIter.remove();
        }
    }
}

它是如何工作的

映射是包含一组键/值对的对象。当您需要存储索引(键)并将其与特定值相关联时,映射会很有用。映射不能包含任何重复的键,并且每个键只能映射到一个值。解决方案(StockPortfolio1.java)的源代码演示了如何在地图中添加和删除条目。它还包含了这个配方的解决方案中列出的源代码,演示了如何使用遗留技术以及利用 lambda 表达式和流的新语法来迭代映射条目。

summary()方法使用 foreach 循环实现来迭代 portfolio map 的条目集。为了使用遗留代码进行迭代,Map entrySet()方法返回一组 Map。条目对象。在循环中,您可以访问当前地图的键和值。条目,方法是在该条目上调用相应的方法 key()和 value()。当您需要在迭代时访问映射键和值,并且不需要从映射中删除元素时,请使用这种迭代方法。看一下新的语法,您会发现相同的迭代可以在一行代码中执行。较新的语法利用了 forEach()方法,该方法被添加到 Java 8 的 Map 接口中。它将 lambda 表达式应用于列表中的每个条目。lambda 表达式将键和值都作为参数,然后将它们打印出来。

alertListLegacy()方法使用 foreach 循环实现来迭代项目组合图的值。Map values()方法返回地图值的集合;在这种情况下,股票的集合。当您只需要访问映射值而不需要从列表中删除元素时,请使用这种迭代方法。类似地,如果您只需要访问映射键(同样,不需要删除元素),您可以使用 keySet()方法进行迭代:

for (String symbol : portfolio.keySet()) {
    ...
}

如果您还需要在使用键集进行迭代时访问 map 值,请避免使用以下方法,因为这非常低效。相反,使用 summary()方法中显示的迭代方法。

for (String symbol : portfolio.keySet()) {
    Stock stock = portfolio.get(symbol);
    ...
}

看一下解决方案中的 alertList()方法,您可以看到使用流、过滤器和收集器的组合可以用少得多的工作来执行相同的迭代。有关流和流 API 的更多详细信息,请参见配方 7-8。在 alertList()中,会生成一个流,然后以 lambda 表达式的形式对该流应用一个过滤器。最后,对过滤器应用一个收集器,创建一个要返回的列表。

remove(List )方法获取代表要从投资组合中删除的股票的股票符号列表。该方法使用 keySet()迭代器遍历投资组合映射键,如果当前映射条目是指定要删除的股票之一,则将其删除。注意,map 元素是通过迭代器的 remove()方法移除的。这是可能的,因为键集由映射支持,所以通过键集迭代器所做的更改会反映在映射中。您还可以使用它的 values()迭代器迭代投资组合图:

Iterator<Stock> valueIter = portfolio.values().iterator();
while (valueIter.hasNext()) {
    if (sellList.contains(valueIter.next().getSymbol())) {
        valueIter.remove();
    }
}

与键集一样,值集合由映射支持,因此通过值迭代器调用 remove()将导致从投资组合映射中删除当前条目。

总之,如果您需要在对 map 进行迭代时从 map 中移除元素,可以使用 map 的集合迭代器之一进行迭代,并通过迭代器移除 map 元素,如 remove(List )方法所示。这是在迭代过程中移除地图元素的唯一安全方式。否则,如果您不需要删除 map 元素,您可以利用 foreach 循环和这个配方的解决方案中显示的一种迭代方法。

7-10.并行执行流

问题

您希望并行地迭代一个集合,以便在多个 CPU 上分配工作。

解决办法

利用集合上的流构造,并调用 parallelStream()作为第一个中间操作,以便利用多个 CPU 处理。下面的类演示 parallelStream()操作的多种用法:

public class StockPortfolio2 {
    static List<Stock> myStocks = new ArrayList();

    private static void createStocks(){
        myStocks.add(new Stock("ORCL", "Oracle", 500.0));
        myStocks.add(new Stock("AAPL", "Apple", 200.0));
        myStocks.add(new Stock("GOOG", "Google", 100.0));
        myStocks.add(new Stock("IBM", "IBM", 50.0));
        myStocks.add(new Stock("MCD", "McDonalds", 300.0));
    }

    public static void main(String[] args){
        createStocks();
        // Iterate over each element and print the stock names
        myStocks.stream()
                .forEach(s->System.out.println(s.getName()));

        boolean allGt = myStocks.parallelStream()
                .allMatch(s->s.getShares() > 100.0);
        System.out.println("All Stocks Greater Than 100.0 Shares? " + allGt);

        // Print out all stocks that have more than 100 shares
        System.out.println("== We have more than 100 shares of the following:");
        myStocks.parallelStream()
                .filter(s -> s.getShares() > 100.0)
                .forEach(s->System.out.println(s.getName()));

        System.out.println("== The following stocks are sorted by shares:");
        Comparator<Stock> byShares = Comparator.comparing(Stock::getShares);
        Stream<Stock> sortedByShares = myStocks.parallelStream()
                .sorted(byShares);
        sortedByShares.forEach(s -> System.out.println("Stock: " + s.getName() + " - Shares: " + s.getShares()));

        // May or may not return a value
        Optional<Stock> maybe = myStocks.parallelStream()
                .findFirst();
        System.out.println("First Stock: " + maybe.get().getName());

        List newStocks = new ArrayList();
        Optional<Stock> maybeNot = newStocks.parallelStream()
                .findFirst();
        Consumer<Stock> myConsumer = (s) ->
        {
          System.out.println("First Stock (Optional): " + s.getName());
        };
        maybeNot.ifPresent(myConsumer);

        if(maybeNot.isPresent()){
            System.out.println(maybeNot.get().getName());
        }

        newStocks.add(new Stock("MCD", "McDonalds", 300.0));
        Optional<Stock> maybeNow = newStocks.stream()
                .findFirst();
        maybeNow.ifPresent(myConsumer);

    }

}

它是如何工作的

默认情况下,操作在串行流中执行。但是,您可以指定 Java 运行时在多个子任务之间分割操作,从而利用多个 CPU 来提高性能。当操作以这种方式执行时,它们是“并行”执行的 Java 运行时可以通过调用 parallelStream()中间操作将流划分为多个子流。当这个操作被调用时,聚合操作可以处理多个子流,然后最终将结果合并。您还可以通过调用操作 BaseStream.parallel 来并行执行流。

摘要

本章介绍了各种数据结构以及如何使用它们。首先,您了解了枚举,并学习了如何有效地利用它们。接下来,我们讲述了数组和 ArrayList 的基础知识,并学习了如何在这些结构中迭代元素。这一章还介绍了 Java 泛型,它允许你将对象类型从容器类型中分离出来,提供更多类型安全和高效的代码。最后,本章介绍了 Streams API,这是 Java 8 发布时引入的最重要的更新之一,用于处理集合。